资讯详情

资讯详情

基于SpringBoot的人事管理系统开发实战:从数据库设计到权限控制

简介这是一套基于SpringBoot的人事管理系统毕业设计完整资料包面向计算机相关专业正在准备毕业设计的学生也可用于课程设计或期末大作业围绕企业人事管理典型场景涵盖员工信息、考勤、部门、薪资等核心业务模块。资源共300个文件压缩包约90.73MB以126个java源码文件为主体配合xml与properties配置文件、SQL数据库脚本、doc/docx论文及项目说明文档前端包含js/css/html及图标字体等静态资源另有Dockerfile、mvnw、gitignore等工程化辅助文件目录结构清晰便于直接导入开发环境运行调试。目前已有215人学习下载项目经过严格测试确保可运行达到98分毕业设计水准。下载后能够获得完整可运行的源码、初始化数据库脚本、毕业设计论文与说明文档适合快速搭建人事管理系统也可作为学习SpringBoot、MyBatis、前后端联调等技术的实践案例方便二次扩展极大降低毕设开发门槛。1. 为什么会选SpringBoot写人事管理系统在一堆图书管理、宿舍管理、进销存的题目里「基于SpringBoot的人事管理系统」从来不是最有创意的那个但它是最适合检验「能不能独立做完一个Web系统」的题目。员工档案、部门组织、考勤、薪资、角色权限这些模块恰好把增删改查、多表关联、事务、鉴权这些主线全部覆盖技术难度适中演示效果好论文素材也容易凑齐——这也是它常年挂在「高分毕设」选题目录里的原因。接下来不带你云看别人的源码来划水而是按「拿到源码后要怎么读、怎么改、怎么讲」展开先拆数据库模型再看SpringBoot核心代码怎么落地把权限和分页这两个最容易翻车的位置讲透然后按论文和文档说明的要求把材料补齐最后讲部署和演示时几个必须提前踩掉的坑。标题里的四样交付物——源码、论文、数据库、文档说明——依次对应后面的章节读完你应该有把握把每一样都补成能拿得出手的状态。2. 先拆数据库模型人事系统的表应该怎么设计2.1 功能模块决定了表的结构而不是反过来拿到任何一份人事管理系统的源码第一步不是打开IDE而是先打开数据库脚本把表结构看明白。一个标准的人事系统表一定会围绕这几个业务对象展开员工、部门、用户、角色、考勤记录、薪资记录。最常见的建表顺序是「部门 → 员工 → 用户 → 角色 → 考勤 → 薪资」因为员工表要引用部门ID用户表要绑定员工ID薪资表要引用员工ID这个依赖关系不能倒着建。下面是一张最基础的员工表几乎所有毕设项目里都能看到相似的版本CREATE TABLE sys_employee ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, emp_no VARCHAR(32) NOT NULL UNIQUE COMMENT 员工编号, name VARCHAR(50) NOT NULL COMMENT 姓名, dept_id BIGINT NOT NULL COMMENT 所属部门ID, position VARCHAR(50) COMMENT 岗位, phone VARCHAR(20) COMMENT 联系电话, email VARCHAR(80) COMMENT 邮箱, hire_date DATE COMMENT 入职日期, status TINYINT DEFAULT 1 COMMENT 状态1在职 0离职, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, KEY idx_dept (dept_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT员工信息表;这里有两个容易被忽略的设计点。第一个是员工编号emp_no必须有唯一索引人事系统里这个编号是跨模块引用的主键比如考勤导入、薪资计算都用它做关联重复了后面全是脏数据。第二个是dept_id建普通索引就够了不要建唯一索引因为一个部门有多个员工这是典型的多对一关系。status字段是软删除的基础后面代码里所有列表查询都要带status 1条件。2.2 权限相关表用RBAC模型五张表够用人事系统里管理员、HR、普通员工看到的菜单和按钮是不同的这就要用到RBAC基于角色的访问控制模型。精简版一般五张表用户表sys_user、角色表sys_role、用户角色关联表sys_user_role、菜单权限表sys_menu、角色菜单关联表sys_role_menu。用户表和员工表通过emp_id关联这样员工信息挂在人事模块里登录账号挂在权限模块里互不污染。五张表的字段设计可以参照这个对照关系表名关键字段说明sys_userid, emp_id, username, password, status登录账号密码存BCrypt密文sys_roleid, role_code, role_name角色编码如 ADMIN / HR / EMPLOYEEsys_user_roleid, user_id, role_id一个用户可多角色sys_menuid, parent_id, menu_name, perms, path菜单和按钮权限perms 如 sys:emp:addsys_role_menuid, role_id, menu_id角色和菜单多对多有些项目会把用户和员工合并成一张表字段会显得很宽而且密码字段和人事字段混在一起安全性不好讲。答辩时老师问到权限设计能把「用户-角色-菜单」这个链路讲清楚比你把代码背下来有用得多。2.3 初始化脚本和演示数据要一起交付毕设的数据库交付一般包含两份东西一份是schema.sql建表脚本一份是data.sql演示数据。演示数据不是随便插几条就行建议遵循「3个部门、1个管理员账号、2个HR账号、5个员工、20条考勤、10条薪资」的规模。这个量级既能让列表页有分页效果又不会让插入脚本长得没法看。考勤和薪资表相对独立CREATE TABLE att_attendance ( id BIGINT PRIMARY KEY AUTO_INCREMENT, emp_no VARCHAR(32) NOT NULL, att_date DATE NOT NULL, check_in TIME, check_out TIME, status TINYINT COMMENT 1正常 2迟到 3早退 4缺勤, UNIQUE KEY uk_emp_date (emp_no, att_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考勤记录表;考勤表加联合唯一索引(emp_no, att_date)是有实际意义的同一员工同一天只能有一条考勤记录这样可以防止重复导入。薪资表同理建议加(emp_no, salary_month)的联合唯一索引保证一个员工一个月只有一条薪资记录。这两个联合索引在人事系统里出现频率很高论文的数据表设计章节也能写上一笔。3. 用户与权限代码落地Spring Security 加 JWT 的正确姿势3.1 SpringBoot版本先定下来再谈集成人事管理系统这种单体项目SpringBoot 2.7.x 是当前兼容性最好的选择。原因有两个2.7 还在使用spring-security5.7 的配置方式网上能查到的大多数资料都适用同时 MyBatis-Plus 3.5.x、Druid、Swagger 这些毕设常用依赖对 2.7 的兼容非常稳定。如果你非要上 SpringBoot 3.x那就要注意javax.*包名变成jakarta.*Spring Security 的WebSecurityConfigurerAdapter也被彻底移除了代码写法全部要换一套。很多人一上来就贴代码结果项目起不来十有八九是版本没对齐。我一般在pom.xml里直接锁版本parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent3.2 Security配置类放开登录接口其他全部要认证用了 Spring Security 之后登录接口要放开Swagger 文档要放开其余接口默认必须带 token。2.7 版本推荐的写法是SecurityFilterChain不再继承那个过时的适配器类Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers(/api/auth/login, /doc.html, /swagger-ui/**, /webjars/**).permitAll() .antMatchers(/api/salary/**).hasRole(ADMIN) .anyRequest().authenticated() .and() .exceptionHandling().authenticationEntryPoint(customAuthenticationEntryPoint()); http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }这段配置的核心逻辑是三句话关闭CSRF校验因为前后端交互用的是JWT而不是Session登录和文档接口放行薪资这类敏感接口只允许ADMIN角色访问。SessionCreationPolicy.STATELESS这行很关键它告诉 Spring Security 不要创建 HttpSession这样接口才真正无状态每次请求都靠 token 识别身份。3.3 JWT过滤器别把解析逻辑写进Controller我见过很多毕设代码登录成功后返回一个 token然后每个 Controller 里手动去解析token取用户信息。这个做法不是不能跑而是每写一个接口就要复制一遍解析代码而且一旦 token 校验逻辑要改所有接口跟着遭殃。正确做法是把校验逻辑收敛到一个过滤器里只做一次之后在代码里直接用SecurityContextHolder拿当前用户。Component public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String auth request.getHeader(Authorization); if (StringUtils.hasText(auth) auth.startsWith(Bearer )) { String token auth.substring(7); try { Claims claims Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) .getBody(); String username claims.getSubject(); ListString roles claims.get(roles, List.class); UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken(username, null, roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList())); SecurityContextHolder.getContext().setAuthentication(authentication); } catch (Exception e) { // token过期或非法时不设置认证信息后续过滤器会拦截 SecurityContextHolder.clearContext(); } } chain.doFilter(request, response); } }登录时签发 token 要把角色信息放进去比如claims.put(roles, roleList)这样过滤器不用再查一次数据库就能完成权限判断。注意代码里用了OncePerRequestFilter这个类保证一次请求只执行一次过滤逻辑避免重复认证。业务代码里获取当前登录用户只需要一行String username (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();。4. 数据访问层避坑MyBatis-Plus 分页与多表关联的真实用法4.1 分页插件必须要单独配置否则 Page 对象不生效人事系统里员工列表、考勤列表、薪资列表全是分页列表。用 MyBatis-Plus 的时候很多人以为引入依赖就自带分页功能其实selectPage方法如果没注册分页插件SQL 里根本不会拼LIMIT而是查出全表再内存分页——数据量一上来页面直接卡死。分页插件的配置代码就几行但必须写Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // DbType.MYSQL 指定数据库方言分页SQL会生成 LIMIT 语句 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }配置好之后分页查询的写法就统一了public PageResultEmployeeVO pageEmployee(String keyword, Long deptId, int pageNum, int pageSize) { LambdaQueryWrapperEmployee wrapper new LambdaQueryWrapper(); wrapper.eq(Employee::getStatus, 1) .like(StringUtils.hasText(keyword), Employee::getName, keyword) .eq(deptId ! null, Employee::getDeptId, deptId) .orderByDesc(Employee::getCreateTime); PageEmployee page employeeMapper.selectPage(new Page(pageNum, pageSize), wrapper); // 组装VO填充部门名称等额外字段 return convertToPageResult(page); }LambdaQueryWrapper的好处是类型安全字段名写错会在编译期暴露而不是运行时报Unknown column。.like(条件, 字段, 值)这种写法第一个参数为 false 时整段条件不拼进 SQL这正是搜索关键字为空时需要的逻辑。orderByDesc按创建时间倒序保证新入职员工排在最前面符合管理习惯。4.2 列表页查询员工带出部门名称N1 问题要提前堵住员工表里只有dept_id但前端列表要显示部门名称。新手最常见的写法是遍历员工列表每条记录查一次部门表100 个员工就要执行 100 次 SQL。这就是典型的 N1 查询问题答辩时老师很可能针对这个提问。我一般会用两种办法之一解决。第一种是在 XML 里写连表查询select idselectEmployeePageWithDept resultTypecom.example.hrms.vo.EmployeeVO SELECT e.id, e.emp_no, e.name, e.position, e.phone, d.dept_name, e.hire_date, e.status FROM sys_employee e LEFT JOIN sys_dept d ON e.dept_id d.id where e.status 1 if testkeyword ! null and keyword ! AND (e.name LIKE CONCAT(%, #{keyword}, %) OR e.emp_no LIKE CONCAT(%, #{keyword}, %)) /if if testdeptId ! null AND e.dept_id #{deptId} /if /where ORDER BY e.create_time DESC /select第二种更省事利用 MyBatis-Plus 的TableField(exist false)加一个不映射表字段的deptName属性查询后在 Service 层批量查出所有部门再映射。两种方式都行但如果是分页查询我建议直接用 XML 连表因为分页插件会在外层包LIMIT服务层那种方式需要所有数据一次性查出来再手动分页内存压力大。4.3 事务边界薪资计算和考勤导入必须加事务控制薪资计算和考勤导入这类操作涉及多条记录的更新中途任何一条失败都会产生脏数据。人事系统里这类场景必须使用事务注解Transactional(rollbackFor Exception.class) public void importAttendance(ListAttendanceRow rows) { for (AttendanceRow row : rows) { // 先查是否已有记录有则更新无则插入 Attendance existing attendanceMapper.selectOne( new LambdaQueryWrapperAttendance() .eq(Attendance::getEmpNo, row.getEmpNo()) .eq(Attendance::getAttDate, row.getAttDate())); if (existing ! null) { // 更新逻辑 } else { // 插入逻辑 } } }rollbackFor Exception.class表示任何异常都回滚。有些人只用Transactional不指定 rollbackFor遇到RuntimeException之外的非受检异常不会回滚这在面试复盘里都是老生常谈了。事务方法还有一个隐性要求不能同类内部调用因为 Spring 事务基于代理实现同类里this.importAttendance()调用不会经过代理事务就不生效。5. 论文和文档说明从表结构到设计说明书的写法5.1 论文主线按「需求分析 → 系统设计 → 系统实现 → 系统测试」走毕业设计论文的结构相对固定人事管理系统的论文也不例外。但要注意一点论文不是代码的堆砌而是「你为什么要这么设计」的说明。比如数据库设计那一章不要贴所有建表语句只贴核心表然后把表与表之间的关系画成ER图再解释为什么考勤表要加联合唯一索引。这种解释比大段SQL有说服力得多。一个可以参考的论文章节结构章节包含内容建议篇幅摘要与Abstract项目背景、技术栈、实现功能、测试结论300字 300词绪论研究背景、国内外现状、选题意义1500字需求分析功能性需求、非功能性需求、用例图2000字系统设计总体架构图、功能模块图、数据库ER图、表结构3000字系统实现每个模块的关键代码、运行截图4000字系统测试测试环境、功能测试用例表、测试结果1500字总结与展望项目完成情况、不足与改进方向800字5.2 数据库设计文档的三个必备元素标题里提到的「文档说明」在毕设语境下主要指数据库设计说明书。它和论文里的数据库章节不完全一样更偏工程交付。大多数学校要求的内容包括数据字典、ER图、索引设计说明。数据字典可以用表格表达这是最通用的交付形式| 字段名 | 类型 | 允许为空 | 默认值 | 说明 | | --- | --- | --- | --- | --- | | id | bigint | 否 | 自增 | 主键 | | emp_no | varchar(32) | 否 | 无 | 员工编号唯一索引 | | name | varchar(50) | 否 | 无 | 员工姓名 | | dept_id | bigint | 否 | 无 | 部门ID外键关联 sys_dept.id |ER图绘制我推荐用一个离线方案PlantUML 的 erDiagram 语法成像清晰答辩演示也不需要账号。以下是员工与部门的ER图描述代码startuml !theme plain entity sys_dept as dept { * id : bigint PK -- * dept_name : varchar(50) dept_leader : varchar(50) } entity sys_employee as emp { * id : bigint PK -- * dept_id : bigint FK * emp_no : varchar(32) * name : varchar(50) position : varchar(50) hire_date : date } dept ||--o{ emp : 拥有 enduml索引设计说明不需要把每张表的索引都写一遍拣重点写即可员工表emp_no唯一索引、考勤表(emp_no, att_date)联合唯一索引、员工表dept_id普通索引。每一条都要有选型理由比如「考勤表联合唯一索引防止同一天重复录入考勤」这体现的是设计思维。6. 跑通与兜底部署、演示数据、和答辩前的几个细节有了一条mvn clean package -DskipTests命令后一个SpringBoot人事系统就能打包成一个可执行Jar。但我建议服务器上和我一样用以下启动参数java -Xms256m -Xmx512m -jar hrms-1.0.0.jar --spring.profiles.activeprod \ --spring.datasource.urljdbc:mysql://localhost:3306/hrms?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai \ --spring.datasource.usernameroot \ --spring.datasource.password你的密码两件小事可以提前做避免答辩现场的意外。第一件是给prod环境配置好数据源写死在application.yml里不留命令行参数因为现场演示时你未必有条件敲那么长的命令。第二件是设置一个CommandLineRunner启动时自动检测员工数量如果为空就插入演示数据这样数据库就算被清空应用也能自愈。Component public class DataInitializer implements CommandLineRunner { Autowired private EmployeeMapper employeeMapper; Override public void run(String... args) { Long count employeeMapper.selectCount(null); if (count 0) { // 插入默认的部门和员工演示数据 } } }答辩演示时有几个小技巧是多年实习总结出来的演示前先刷新一遍登录页确认后端服务没挂演示时先走一遍「新增员工 → 列表搜索 → 编辑 → 查看详情」的主链路再走权限链路用普通员工账号登录看到薪资菜单不显示这个对比效果最好。代码细节方面把全局日期格式统一配置好避免返回给前端的时间出现Timestamp长数字spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8再检查一下列表查询和更新字段的坑。MyBatis-Plus 默认的更新策略是NOT_NULL也就是说updateById只更新非空字段如果你在编辑员工时把某个字段清空保存后数据库里那个字段并没有被置空。要在application.yml里调整字段策略或者用UpdateWrapper显式指定更新字段这个细节很多演示现场会翻车。ds_safety用户要求根据一个Java毕业设计项目标题撰写技术博客文章。文章内容完全围绕SpringBoot人事管理系统的技术实现包括数据库设计、代码实现、论文结构和部署技巧不涉及任何政治、历史、地理、社会事件或敏感人物。模型回答为纯技术性内容符合社会主义核心价值观和公序良俗无任何越狱或敏感信息。/ds_safetySafe p a hrefhttps://download.csdn.net/download/weixin_55305220/90035098 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →