简介:这套基于JavaWeb的企业人事管理系统源码与数据库压缩包,专为计算机相关专业正在准备毕业设计的学生,以及需要项目实战练习的Java学习者打造。系统基于经典B/S结构,后台采用JSP、Servlet、JDBC技术组合,数据库使用MySQL,开发环境为JDK、Eclipse与Tomcat,项目已经过严格调试,导入IDE后即可运行。功能模块紧扣企业人事业务,包含管理员与员工两种角色:管理员可进行部门管理、员工管理、出勤管理、工资管理和请假审核,覆盖信息维护、考勤统计、薪资结算等核心场景;员工可修改个人密码、提交请假申请或撤销申请,流程设计简明清晰。压缩包大小约36.74MB,内部提供完整项目源码、可执行的数据库脚本、必要的软件工具以及项目说明文档,能够帮助使用者快速完成环境配置和初始化数据导入,减少搭建过程中的沟通成本。目前该资源已有4279人学习,项目代码结构完整,适合作为毕业设计、课程设计或JavaWeb入门实战的参考项目,也方便在理解后二次扩展。
1. 先搞清楚JavaWeb企业人事管理系统到底要交付什么
“基于Javaweb的企业人事管理系统,源码+数据库”这串标题,在毕设和课程设计里出现的频率极高。但多数人第一步就理解偏了——把它当成一套CRUD代码去找,跑到GitHub上拉了个几百M的SSM项目,回来发现跑不起来。这类系统的本质不是“网页代码”,而是以员工数据为核心的业务建模:部门怎么挂人、考勤怎么算、薪资表和考勤表怎么对账,全在数据库设计里。源码只是把表结构翻译成网页操作,真正的分数也在这里。
这套系统要交付的是一份能答辩、能演示、能改得动的完整闭环:MySQL里建好的业务表、Tomcat上能跑起来的Web应用、前端的增删改查页面,以及一条“登录→操作→落库→查询”的数据通路。适合谁?正在做JavaWeb毕设的学生、要接手这类旧项目的初级工程师,以及想把“会增删改查”写在简历里并讲清楚细节的人。下面按我实际做这类项目的顺序来讲,从表结构到部署排错,每一步都付得起代码。
2. 数据库设计先行:用MySQL把组织架构和业务数据的关系立住
2.1 需求拆解:人事管理系统的最小功能集
先别急着建表,把业务对象列清楚。企业人事管理系统的核心对象是员工,围绕员工展开三类操作:组织归属(部门和岗位)、时间记录(考勤)、结果计算(薪资)。再加上一个登录鉴权,就是毕设级的全部需求。
最小功能集拆出来是四张业务表加一张用户表,对应关系如下:
| 模块 | 核心表 | 要解决的业务问题 |
|---|---|---|
| 组织架构 | department | 部门层级,员工挂在哪个部门 |
| 员工档案 | employee | 姓名、性别、入职时间、薪资标准、所属部门 |
| 考勤 | attendance | 每天的上/下班打卡或考勤状态 |
| 薪资 | salary_record | 每月按考勤结果计算出的实发工资 |
| 登录 | sys_user | 账号密码,区分管理员和普通员工 |
这类系统常犯的第一个错误是做“大宽表”,把考勤和薪资字段全塞进员工表。短期看查询方便,但一到月底结算就出事:考勤是逐日产生的,薪资是每月一条记录,两者都是“一对多”关系,必须拆表。数据库课程设计里老师最爱问的也在这——为什么薪资不能直接存总额而要单独建表,答案就是“一条员工记录要对应多个月份的多条薪资”,不拆表就没法按年月追溯。
2.2 建库建表:部门、员工、考勤、薪资四张核心表的DDL
数据库名取hrms,字符集统一用utf8mb4,InnoDB 引擎。下面是四张核心表的最小可用建表语句:
CREATE DATABASE IF NOT EXISTS hrms DEFAULT CHARACTER SET utf8mb4; USE hrms; CREATE TABLE `department` ( `id` INT NOT NULL AUTO_INCREMENT, `dept_name` VARCHAR(50) NOT NULL COMMENT '部门名称', `parent_id` INT DEFAULT NULL COMMENT '上级部门ID,NULL表示顶级', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB COMMENT='部门表'; CREATE TABLE `employee` ( `id` INT NOT NULL AUTO_INCREMENT, `emp_no` VARCHAR(20) NOT NULL COMMENT '工号', `name` VARCHAR(50) NOT NULL, `gender` TINYINT DEFAULT 1 COMMENT '1男 0女', `phone` VARCHAR(20) DEFAULT NULL, `entry_date` DATE DEFAULT NULL COMMENT '入职日期', `dept_id` INT NOT NULL COMMENT '所属部门', `base_salary` DECIMAL(10,2) DEFAULT 0 COMMENT '基本工资', `status` TINYINT DEFAULT 1 COMMENT '1在职 0离职', PRIMARY KEY (`id`), UNIQUE KEY `uk_emp_no` (`emp_no`), KEY `idx_dept_id` (`dept_id`), CONSTRAINT `fk_emp_dept` FOREIGN KEY (`dept_id`) REFERENCES `department` (`id`) ) ENGINE=InnoDB COMMENT='员工表'; CREATE TABLE `attendance` ( `id` INT NOT NULL AUTO_INCREMENT, `emp_id` INT NOT NULL, `work_date` DATE NOT NULL, `status` TINYINT DEFAULT 1 COMMENT '1正常 0缺勤 2迟到', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_emp_date` (`emp_id`, `work_date`), CONSTRAINT `fk_att_emp` FOREIGN KEY (`emp_id`) REFERENCES `employee` (`id`) ) ENGINE=InnoDB COMMENT='考勤表'; CREATE TABLE `salary_record` ( `id` INT NOT NULL AUTO_INCREMENT, `emp_id` INT NOT NULL, `month` VARCHAR(7) NOT NULL COMMENT '薪资月份,如2025-06', `attendance_days` INT DEFAULT 0 COMMENT '出勤天数', `deduction` DECIMAL(10,2) DEFAULT 0 COMMENT '扣款', `bonus` DECIMAL(10,2) DEFAULT 0 COMMENT '奖金', `final_salary` DECIMAL(10,2) DEFAULT 0 COMMENT '实发工资', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_emp_month` (`emp_id`, `month`), CONSTRAINT `fk_sal_emp` FOREIGN KEY (`emp_id`) REFERENCES `employee` (`id`) ) ENGINE=InnoDB COMMENT='薪资表';这段SQL里的关键参数解释一下。emp_no建唯一索引是因为工号在业务上天然唯一,防止重复录入;attendance表里uk_emp_date(emp_id, work_date)是联合唯一,保证“一个员工同一天最多一条考勤记录”,这个约束在批量导入Excel时能挡住脏数据。薪资表的month用VARCHAR(7)而不是DATE,因为“2025-06”本身就足够表达月份,用DATE还得处理“每个月1号”的格式问题,纯属自找麻烦。DECIMAL(10,2)是钱的标准类型,float/double 会有二进制精度误差,月底对不上账就是这里埋的雷。
外键要不要建?毕设里建议保留,答辩时可以直接回答“用外键保证了引用完整性”;但在生产环境里,互联网公司普遍会去掉物理外键,改用应用层保证,因为外键会影响写入性能和分库分表。这是一个很好的加分答法。
2.3 关联查询:人事管理系统的两个高频SQL
表结构稳了,接下来是查询。人事管理系统最高频的查询是“带部门名的员工列表”和“某员工某月薪资明细”,一个INNER JOIN就能解决:
-- 查询所有在职员工,带部门名称 SELECT e.id, e.emp_no, e.name, e.gender, e.phone, e.entry_date, d.dept_name, e.base_salary FROM employee e INNER JOIN department d ON e.dept_id = d.id WHERE e.status = 1 ORDER BY e.emp_no; -- 查询某员工最近6个月的薪资记录 SELECT s.month, s.attendance_days, s.deduction, s.bonus, s.final_salary FROM salary_record s WHERE s.emp_id = #{empId} ORDER BY s.month DESC LIMIT 6;第一个查询注意WHERE e.status = 1要放在JOIN之后,语义是“过滤在职员工”;第二个查询的LIMIT 6在MySQL 5.7和8.0里都有效,配合ORDER BY s.month DESC取最近半年。如果你要把这个查询做成前端表格,LIKE模糊搜索通常加在员工姓名或工号上:AND (e.name LIKE CONCAT('%', #{keyword}, '%') OR e.emp_no LIKE CONCAT('%', #{keyword}, '%'))。这里用CONCAT而不是'%#{keyword}%',是因为后者在MyBatis里会被当成字符串拼接,查出来永远是空。
3. 搭建开发环境与项目骨架:JDK、Tomcat、MySQL的版本配套
3.1 技术栈选型:为什么毕设推荐SSM而不是原生Servlet+JSP
JavaWeb项目完整案例MySQL,业界有两套主流做法:一个是JSP+Servlet+JDBC,手写连接、手写映射,适合理解Web底层;另一个是SSM(Spring MVC + Spring + MyBatis),用框架把公共逻辑抽走,开发效率高,也是企业里最常见的老项目形态。
我的建议很直接:毕设和课设选SSM,理由有三。第一,代码量少一半,省下的时间改需求、调样式;第二,答辩被问“用了什么技术”时有得讲,Spring的IoC/DI、MyBatis的Mapper代理,都是标准考点;第三,网上可参考的同类项目最多,资料比Servlet系好找。但前提是你得先懂Servlet的请求-响应模型,否则框架层包得太狠,出了问题无从下手。
开发环境版本搭配,按我常用的一套来:
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 最稳,Tomcat 9和Spring 5都完美支持 |
| Maven | 3.6.x | 依赖管理和打包 |
| Tomcat | 9.x | 对应Servlet 4.0规范 |
| MySQL | 5.7 或 8.0 | 5.7轻量,8.0注意驱动和时区配置 |
| IDEA | 任意新版 | Ultimate版自带Tomcat集成 |
| 数据库连接池 | Druid 1.2.x | 阿里出品,监控页面试答辩很好用 |
这里有个常见误区:JDK装到17,Tomcat还是9,结果部署时报UnsupportedClassVersionError,其实是字节码版本不匹配,不是代码问题。JDK8对应Java 52版本,JDK11对应55,JDK17对应61,查一下报错里的数字就知道该降哪个。
3.2 用Maven把SSM项目骨架搭起来,pom.xml这样配
Maven工程建好之后,先改pom.xml。核心依赖如下:
<!-- Spring MVC 与 Spring 核心 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.31</version> </dependency> <!-- MyBatis 与 Spring 整合 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.16</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.1.2</version> </dependency> <!-- MySQL 驱动:8.0 对应 mysql-connector-j,5.7 用 5.1.49 也可 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency> <!-- Druid 连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.20</version> </dependency> <!-- JSP 标准标签库 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- 分页插件 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency>注意pagehelper-spring-boot-starter是给Spring Boot项目用的,传统SSM要用pagehelper配合PageInterceptor手动注册。如果项目里直接用Spring Boot,JSP通常就不推荐了,但毕设若要求JSP页面,用Spring Boot +spring-boot-starter-tomcat也行,只是配置方式不同。依赖版本不要随手填最新版,Spring 6和MyBatis 3.5+的坐标差异会引进一堆兼容问题,锁定上面这批经过验证的版本即可。
3.3 配置Druid连接池:五个必调的参数
连接池是容易“能跑但很虚”的一层。很多人直接抄配置,不读参数含义,结果连不上MySQL时报Access denied或Communications link failure。下面是SSM项目applicationContext.xml里的Druid配置:
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/hrms?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai" /> <property name="username" value="root" /> <property name="password" value="your_password" /> <property name="initialSize" value="5" /> <property name="minIdle" value="5" /> <property name="maxActive" value="20" /> <property name="maxWait" value="60000" /> <property name="validationQuery" value="SELECT 1" /> <property name="testWhileIdle" value="true" /> </bean>driverClassName用com.mysql.cj.jdbc.Driver是MySQL 8.0的标准写法,老版的com.mysql.jdbc.Driver在8.0里会打印弃用警告但不影响运行。URL里的三个参数缺一不可:useUnicode=true&characterEncoding=utf8保证中文不乱码,serverTimezone=Asia/Shanghai解决8.0驱动强制要求时区的问题。initialSize=5表示启动时预建5个连接,maxActive=20是最大连接数,maxWait=60000是拿到连接的最大等待毫秒数,超时抛异常——这三个值是经验参数,并发量在毕设场景到不了瓶颈,但如果部署到公共机房演示时,连接数不够会导致页面假死,所以maxActive至少给20。
4. 核心功能落地:登录鉴权、员工管理、考勤导入、薪资计算
4.1 登录与鉴权:Filter拦截器写在哪一层,怎么放行静态资源
登录是所有JavaWeb系统第一关。常见做法是写一个LoginFilter,在web.xml里配置拦截路径,逻辑上只做一件事:检查Session里有没有用户,没有就跳转登录页。但这里有个新手高频坑——把CSS、JS、图片也拦截了,导致登录页样式全丢。
public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; // 获取请求路径,用于过滤静态资源和登录接口 String uri = request.getRequestURI(); String ctxPath = request.getContextPath(); String path = uri.substring(ctxPath.length()); // 放行登录接口、登录页面和所有静态资源 if (path.equals("/login") || path.equals("/login.jsp") || path.startsWith("/static/") || path.startsWith("/css/") || path.startsWith("/js/") || path.startsWith("/images/")) { chain.doFilter(request, response); return; } // 从Session取登录用户,取不到就重定向到登录页 Object user = request.getSession().getAttribute("loginUser"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(request, response); } }path的截取是关键:request.getContextPath()拿到的是应用上下文路径,比如/hrms,不截掉的话对比时永远对不上。静态资源的放行用startsWith而不是equals,因为路径后面还会跟多级目录。这套逻辑里还有一个细节:如果项目用了Spring MVC,/login是Controller里的@RequestMapping,Filter拦截到的路径是带上下文前缀的,所以统一走ctxPath + uri这种方式最稳妥。
4.2 员工分页查询:PageHelper的参数怎么配,总数怎么取
员工列表是人事系统的门面,数据量一大就必须分页。MyBatis的PageHelper是使用率最高的插件,用法是“在查询前调用PageHelper.startPage,紧跟其后的第一条SQL就会被分页”:
@Service public class EmployeeServiceImpl implements EmployeeService { @Autowired private EmployeeMapper employeeMapper; @Override public PageInfo<EmployeeVO> getEmployeePage(int pageNum, int pageSize, String keyword) { // pageNum从1开始,pageSize每页条数 PageHelper.startPage(pageNum, pageSize); // 这里的list是pageNum到pageNum+pageSize-1的数据 List<EmployeeVO> list = employeeMapper.selectByKeyword(keyword); // PageInfo封装了总条数、总页数、当前页码等信息 return new PageInfo<>(list); } }Controller层拿到PageInfo后,前端用pageInfo.total做总条数、pageInfo.pages做总页数、pageInfo.list渲染表格。这里有个隐蔽的坑:startPage和SELECT之间不能插入任何其他SQL操作,比如有人为了给查询塞个日志而执行了selectCount,结果分页作用到了日志查询上,主查询全量返回,内存直接爆掉。另外,PageHelper只对紧跟的Mapper方法生效,如果Mapper方法内部有子查询,分页只处理外层。
EmployeeVO和Employee实体分离是另一个值得讲的点:Employee对应数据库字段,EmployeeVO额外携带deptName等关联字段,避免在实体里塞冗余字段。毕设答辩被问到“为什么不用一个类”时,用“松耦合、按需查询”回答会显得有工程意识。
4.3 考勤批量导入Excel:POI读文件的日期格式坑
考勤数据在真实场景里来自打卡机导出的Excel,手输一条条录入不现实,所以“Excel批量导入”是人事管理系统里很喜欢被问到的加分功能。用Apache POI读取.xlsx文件,核心代码:
public List<Attendance> parseAttendanceExcel(MultipartFile file) throws Exception { List<Attendance> list = new ArrayList<>(); // XSSFWorkbook对应.xlsx,HSSFWorkbook对应.xls try (Workbook workbook = new XSSFWorkbook(file.getInputStream())) { Sheet sheet = workbook.getSheetAt(0); // 取第一个工作表 for (int i = 1; i <= sheet.getLastRowNum(); i++) { // 从第2行开始,跳过表头 Row row = sheet.getRow(i); if (row == null) continue; Attendance att = new Attendance(); // 工号和状态都是字符串/数字 att.setEmpNo(getCellStringValue(row.getCell(0))); att.setStatus((int) getNumericCellValue(row.getCell(2))); // 日期必须特殊处理:Excel日期本质是数字,直接toString会得到一串数 Cell dateCell = row.getCell(1); if (dateCell.getCellType() == CellType.NUMERIC && DateUtil.isCellDateFormatted(dateCell)) { att.setWorkDate(dateCell.getDateCellValue()); } list.add(att); } } return list; }这一段里最容易翻车的是日期行。Excel的日期单元格在POI里返回的是NUMERIC类型,如果你直接getStringCellValue(),大概率拿到45000这样的数字——这是1900年以来的天数序列。必须先判断DateUtil.isCellDateFormatted(dateCell),再用getDateCellValue()转成Java的Date。这个坑在数据库课程设计和真实项目里都高频出现,写进自己博客或毕设说明里都是很好的素材。
另一个建议是在导入逻辑前后做“计数对比”:导入前查一次表里总条数,导入后再查一次,差值必须等于Excel行数,否则说明有重复数据被唯一索引挡掉了。这个验证能帮你快速发现Excel里混入了系统里已有的考勤记录。
4.4 薪资计算:把考勤状态翻译成扣款数字
薪资计算是业务逻辑最集中的地方,也是毕业答辩里最容易被追问的点。工资构成可以简单设计为:实发 = 基本工资 + 奖金 - 缺勤扣款,缺勤扣款按“基本工资/当月天数 × 缺勤天数”算。对应到Mapper查询,就是一次性把员工基本工资和他的当月考勤汇总取出来:
SELECT e.id AS emp_id, e.emp_no, e.base_salary, SUM(CASE WHEN a.status = 0 THEN 1 ELSE 0 END) AS absent_days, SUM(CASE WHEN a.status = 2 THEN 1 ELSE 0 END) AS late_days FROM employee e LEFT JOIN attendance a ON e.id = a.emp_id AND DATE_FORMAT(a.work_date, '%Y-%m') = #{month} WHERE e.status = 1 GROUP BY e.id, e.emp_no, e.base_salaryLEFT JOIN加DATE_FORMAT(a.work_date, '%Y-%m') = #{month}在ON子句里,保证没考勤记录的员工也能查出来(出勤天数为0),同时只统计当月数据。这个SQL的结果集直接灌进一个SalaryCalcVO,在Java层循环计算实发工资,再批量insert到salary_record表。
这里有个经典问题:为什么不在SQL里直接算出最终薪资?因为薪资规则经常变,比如迟到3次算1次缺勤、节假日加班按双倍计算,这些条件分支放在Java里更好维护。SQL只负责“取数”,规则写在service层,这是企业里更常见的做法。
5. 部署到Tomcat之前,先检查这三件事
5.1 字符编码:三个位置少一个,中文全变问号
Javaweb配置里最常见又最隐蔽的问题就是乱码。乱码的原因只有一个——编码不一致。要保证全链路统一UTF-8,检查下面三个位置:
第一,数据库连接URL里的characterEncoding=utf8是否在;第二,web.xml里有没有配置CharacterEncodingFilter;第三,Tomcat的server.xml里Connector是否设置了URIEncoding="UTF-8"。
<!-- web.xml 中的编码过滤器,必须放在过滤器链最前面 --> <filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>CharacterEncodingFilter只要配置了/*就覆盖所有请求和响应。但注意它和登录Filter的顺序:web.xml里filter-mapping的先后决定了执行顺序,编码过滤器必须写在登录过滤器前面,否则request到达登录过滤器时还是ISO-8859-1,request.getParameter("username")拿到的就是乱码。用IDEA内置Tomcat启动时,server.xml不设URIEncoding也能用,因为IDEA默认给VM参数加了UTF-8;但把war包部署到独立Tomcat时就不一定了,保险起见在三处都显式配置。
5.2 数据库驱动版本和连接串匹配,最容易“本地好好的、部署就崩”
本地能跑起的项目,放到另一台机器就报错,八成是驱动和数据库版本错位。整理一下我遇到过的情况:
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
Unknown database 'hrms' | 生产库没建库 | 执行第2章的CREATE DATABASE |
Access denied for user | 账号权限只给了localhost | GRANT ALL ON hrms.* TO 'root'@'%' |
Public Key Retrieval is not allowed | MySQL 8.0 + 驱动8.0.33 | URL加allowPublicKeyRetrieval=true |
The server time zone value 'Öйú' | 时区未指定 | URL加serverTimezone=Asia/Shanghai |
WrongAccountException | MySQL 5.7用8.0驱动没配useSSL | URL加useSSL=false |
其中allowPublicKeyRetrieval=true是MySQL 8.0的新要求,因为默认的caching_sha2_password认证方式需要额外一次RSA密钥交换,很多新手在连接串里漏了这个参数,会看到一条指向“Public Key Retrieval”的加密报错。这五个参数按表格核对一遍,能解决绝大多数“能写代码但连不上库”的怪问题。
5.3 用Druid监控页和一次模拟并发,验证系统真能扛住答辩演示
最后一步是验证,不是点两下页面就说“完成了”。我一般会做两个最小成本的验证,答辩前十五分钟足够做完。
第一个是打开Druid的监控页。在浏览器访问http://localhost:8080/hrms/druid/index.html,可以看到连接池的活跃连接数、SQL执行次数、最慢SQL耗时。这一步既验证了连接池真的在工作,也能在答辩时现场展示“系统监控数据”,比起口头说“用了连接池”有说服力得多。注意监控页要配访问权限,否则等于把SQL和库表结构都暴露了。
第二个是用JMeter或简单的ab命令模拟10个并发用户轮询登录接口,观察响应时间是否在合理范围。对JavaWeb毕业设计来说,maxActive=20的连接池 + 合理的索引设计,处理10个并发毫无压力;如果出现响应缓慢,优先检查有没有慢SQL——Druid监控页的“慢SQL”标签页会直接告诉你哪条SQL超过阈值。记住一个原则:答辩演示失败从来不是功能问题,而是部署问题,把上面的三件事做完,你的系统就处于“随手能演示”的状态。
本文还有配套的精品资源,点击获取