SpringBoot+Vue人力资源管理系统:从数据库设计到前后端部署全解析
2026/9/24 18:35:37 网站建设 项目流程

做课设或者毕设选题的时候,总有人纠结“到底选个什么项目才既不会太简单拿不出手,又不会太难做不完”。如果你正在Java全栈这条路上,我强烈建议你认真看看SpringBoot+Vue这套组合。这不是什么黑科技,但它恰好覆盖了后端接口开发、前端页面交互、数据库设计这几块硬功夫。今天分享的这套人力资源管理系统(HRMS),是我自己梳理过的、非常适合拿来二次开发或者直接交作业的源码级项目。

先说这个项目解决什么问题:它把一个企业里人力资源部门日常要干的事——“员工信息管理、部门组织架构、考勤打卡、薪资核算、招聘流程、系统用户权限”全部搬到线上,做成一个前后端分离的Web管理平台。对你来说,做完它,你等于亲手走通了一条“数据库建模→后端接口→前端联调→部署上线”的完整链路,这正是企业里真实项目的标准流程。

我在给一些学生朋友改这个项目的时候,发现大家拿到源码最大的痛点不是看不懂代码,而是不知道整个系统是怎么串起来的。所以我这篇分享会从整体设计思路、数据库表结构、后端核心逻辑、前端页面实现,到本地部署的完整步骤,一步步拆给你看。不管你是纯小白照着做,还是想改造成自己的毕设题目,这篇文章都能给你一个清晰的地图。

需要说明的是,文中的代码和方案都基于我实际验证过的一套常见实践,具体到你自己的运行环境,可能需要做少量适配,但思路是完全通用的。

1. 整体设计思路:为什么这套技术栈值得你选

1.1 技术选型的底层逻辑

很多同学选技术栈的时候只看“流行”,但很少想“为什么”。SpringBoot+Vue+MySQL这套组合,能够成为课设和毕设的“标准答案”,背后是有原因的。

先看后端。SpringBoot的核心价值在于“约定大于配置”,它把Spring家族繁琐的XML配置全部自动化了。你写一个查询员工的接口,只需要一个Controller类加一个Service方法,不用像老一代SSH框架那样配一堆Bean。这对课设阶段来说是巨大的效率提升,因为你的时间应该花在理解业务逻辑上,而不是耗在环境配置里。

再看前端。Vue的出现,让页面开发从“操作DOM”转向了“操作数据”。你只需要维护一份数据对象,页面会自动跟着变。比如员工列表,你只要把后端返回的数组赋值给data里的list变量,表格就渲染出来了,不需要手动去拼接HTML字符串。这种开发体验对新手极度友好,而且Vue的生命周期、组件通信这些概念,面试的时候还能拿出来说道说道。

MySQL就更不用说了,开源、免费、资料多,遇到任何问题都能在社区找到答案。这套组合还有一层好处——它和目前绝大多数企业的实际技术栈高度重合。你毕业以后去面试Java开发岗,大概率会碰到这套东西。做课设的同时顺便把面试基础打了,这账怎么算都不亏。

1.2 系统功能模块怎么规划

人力资源管理系统这个题目,看似简单,但里面可以做的点非常多。我在规划这套源码的时候,把它切成了六大功能块。这里要提醒一下,功能不是越多越好,而是要形成一个完整的业务闭环。

第一个是员工管理。这是整个系统的数据核心,所有模块都围绕员工数据展开。包括员工的基本信息增删改查、条件搜索、导入导出(可选)、员工状态管理(在职/离职/试用期)。

第二个是部门管理。一般做成树形结构,因为公司组织架构是分级别的,比如总公司下面有技术部、人事部,技术部下面又有前端组、后端组。这个模块能体现你对递归数据的处理能力,面试官经常问“树形结构怎么存储和查询”。

第三个是考勤管理。这块可以作为亮点扩展。基础版本就是记录上下班打卡时间,进阶一点可以加上请假审批流程。我会在后面的章节详细讲一套可落地的考勤设计思路。

第四个是薪资管理。根据员工基本工资、考勤天数、社保公积金、个税等参数自动计算实发工资。这里涉及金额计算,对精度要求高,我会告诉你怎么避免出现“0.1+0.2不等于0.3”这种经典问题。

第五个是招聘管理。包括职位发布、简历投递记录、面试状态流转。这个模块能让你把状态机的概念应用到实际业务里,比如简历状态从“待筛选”到“面试中”再到“已录用”。

第六个是系统管理。这是做管理平台的标配,包括用户登录注册、角色权限分配、菜单管理。这个模块决定了你的系统是“一个能跑的小Demo”还是“一个真正可用的系统”。我强烈建议你在毕设里加上JWT认证和RBAC权限控制,这绝对是答辩加分项。

2. 数据库设计:地基打牢,后面才不塌

2.1 核心表结构设计

数据库设计是整个项目里最重要、也最容易被忽视的环节。很多同学上来就写代码,写到后面发现表结构不对,推倒重来,非常痛苦。我设计的这套HRMS数据库,总共有8张核心表。这里我挑几张最关键的给大家详细分析。

员工表(employee)是整个系统的核心,我设计的核心字段包括id、emp_code(员工工号)、name、gender、birthday、phone、email、dept_id(所属部门)、position(职位)、salary_base(基本工资)、hire_date(入职日期)、status(在职状态)。注意到没有,我没有直接在员工表里存部门名称,而是存了一个dept_id。这就是数据库设计里的“范式”思想——数据冗余越少越好。部门改名了,员工表不需要动,部门名称始终从部门表实时关联查出来。

部门表(department)就要体现树形结构了,字段设计为id、parent_id(上级部门id,顶级部门这个值为0)、name、leader(负责人)、phone。这里一定要加parent_id,这样就能用一条SQL递归查询出完整的组织架构树。我有一次看到有人设计部门表的时候,用“level字段”来区分层级,后来部门层级调整时数据全乱了。用parent_id才是正解,level可以作为一个冗余字段辅助查询,但不能作为主要关联依据。

考勤表和薪资表需要特别设计一下。考勤表(attendance)字段为id、emp_id、att_date(日期)、clock_in_time(上班打卡时间)、clock_out_time(下班打卡时间)、status(正常/迟到/早退/缺卡)。这里有个细节,为什么不直接存“是否迟到”这种计算结果,而是存原始打卡时间?因为迟到与否是可以动态定义的,比如公司规定9点上班,但某天弹性上班变成9点半了,如果你存的是“是否迟到”这个结果,历史数据就全部错了。存原始时间,计算规则随时可变,这就是“数据与规则分离”的思想。

薪资表(salary)字段为id、emp_id、year、month、base_salary、bonus(奖金)、deduction(扣款)、insurance(社保)、fund(公积金)、actual_salary(实发工资)、create_time。实发工资这个字段一定是计算后落库的,方便后续对账查询,不能每次临时算。

2.2 为什么建议用逻辑删除而不是物理删除

这是个很经典的设计决策。以员工表为例,如果某天操作员手滑删掉了一个员工,而这个员工关联着历史考勤、薪资记录,那这些关联数据就全悬空了,查询全报错。所以我强烈建议在每张业务表上都加一个deleted字段,默认0,删除时执行的是UPDATE employee SET deleted = 1 WHERE id = ?,而不是DELETE。

我在源码里已经把这种逻辑删除的思路统一实现了。这样做最大的好处是:数据永远在,只是标记为不可见,方便数据恢复和审计追踪。代价就是写查询SQL的时候要记得加条件AND deleted = 0,如果用的是MyBatis-Plus,可以在全局配置里配好逻辑删除字段,它会自动帮你拼上这个条件,非常省心。

提示:这个设计细节在你的项目文档和答辩PPT里一定要提。老师问“你这个系统怎么防止误删数据”,你答“我们采用了逻辑删除机制”,这就是一个很好的加分点。

3. 后端核心实现:SpringBoot是怎么把接口串起来的

3.1 项目分层结构与启动入口

SpringBoot项目拿到手第一步,先看包结构。我的源码里把包分成了controller(接收前端请求)、service(业务逻辑)、mapper(数据库操作)、entity(实体类)、config(配置类)、common(通用工具和返回值)、security(安全认证相关)。

这个分层不是摆着好看的。它的核心原则是“上层依赖下层,下层不知道上层的存在”。Controller只负责“收参数、调Service、返回结果”,不写任何业务逻辑。Service专注处理核心业务,只在需要数据时调用Mapper接口。这样做的好处是:如果以后想加一个缓存层,只需要在Service里动手,Controller完全不用改。

启动类上必须有@SpringBootApplication注解,它实际上是一个组合注解,包含了@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan。你只需要在启动类上加上@MapperScan("com.example.hrms.mapper"),MyBatis就能自动扫描到所有的Mapper接口,不用每个都加@Mapper注解。

3.2 登录认证与权限控制的最小实现

一个管理平台第一件事就是登录认证。我在源码里用的是JWT(JSON Web Token)方案。JWT的核心思想是:用户登录成功后,服务端生成一个加密的Token串返回给前端,前端每次请求都把这个Token放在请求头里带上,服务端通过解析Token来确认用户身份。

为什么不用传统的Session方案?因为前后端分离架构下,前端可能部署在A服务器,后端部署在B服务器,Session存在服务端内存里没法共享。而JWT是无状态的,服务端不存任何东西,只靠验签就能确认身份。

JWT实现起来也很简单。用户登录时,把用户名和密码接收过来,用BCrypt算法校验密码(注意,密码绝对不能明文存储,一定要加密),校验通过后,用JWT工具类生成一个Token,Token里可以放userId、userName、roleId这些关键信息。代码大概这样:

// 登录成功后生成JWT String token = Jwts.builder() .setSubject(user.getUsername()) .claim("userId", user.getId()) .claim("roleId", user.getRoleId()) .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24)) // 24小时过期 .signWith(SignatureAlgorithm.HS256, secretKey) .compact();

然后写一个拦截器或者过滤器,拦截所有需要登录才能访问的接口。在拦截器里校验Token是否有效、是否过期,然后把解析出来的用户信息放到请求上下文里,方便后续业务代码获取当前登录人。这里要注意,登录接口本身、静态资源这些路径一定要在拦截器里放行,不然就会出现“还没登录就被拦下来导致没法登录”的尴尬情况。

权限控制我用的是RBAC模型(基于角色的访问控制),核心是三张表:用户表、角色表、权限表,加上用户-角色关联表、角色-权限关联表,一共五张。它的逻辑是:用户不能直接绑定权限,而是先给用户分配角色,再给角色分配权限。比如“HR专员”这个角色可以查员工列表,但不能删除员工;“HR管理员”角色既能查也能删。这样改权限的时候只需要调整“角色-权限”关系,不需要一个个改用户。

3.3 员工模块的增删改查如何写才算合格

员工模块是所有课设里必然有的模块,但很多人的写法停留在“能跑就行”。我建议你按照“加分标准”来写。Controller层接收参数后,一定要做参数校验(用@Validated注解),比如姓名不能为空、手机号要符合格式。Service层要处理核心业务逻辑,例如新增员工时,员工工号要自动生成,建议按照“EMP+当前时间戳的后六位”这种方式生成,保证唯一性。

分页查询是必考的点。MyBatis-Plus提供了一个非常方便的分页插件,代码量极少:

IPage<Employee> page = new Page<>(currentPage, pageSize); LambdaQueryWrapper<Employee> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.isNotBlank(name), Employee::getName, name) .eq(deptId != null, Employee::getDeptId, deptId) .orderByDesc(Employee::getCreateTime); employeeMapper.selectPage(page, wrapper);

这段代码里面的like和eq方法,第一个参数是boolean条件,条件为true才拼上这个查询条件。这样搜索时用户填了什么就查什么,没填就不加条件,非常优雅地处理了动态查询问题。

还有一个细节:如果你不是用MyBatis-Plus,而是原生MyBatis,分页要手写LIMIT ? OFFSET ?,还容易出错。所以课设阶段我非常推荐MyBatis-Plus,它兼容MyBatis,学习成本又低,写代码效率高得多。唯一要注意的是,进行连表查询的时候,最好还是写原生的SQL,因为MP的Wrapper封装不适合复杂的多表关联。

3.4 考勤和薪资模块的常见业务难点

考勤模块最核心的一个问题就是:怎么判断迟到早退?我在具体实现的时候,配置文件里放一个上下班时间的配置项,比如上班时间是09:00:00。查询考勤列表时,用Java的LocalTime去比较员工打卡时间和标准时间:

// 判断是否迟到 LocalTime standardTime = LocalTime.parse("09:00:00"); LocalTime clockIn = record.getClockInTime(); if (clockIn.isAfter(standardTime)) { record.setStatus("迟到"); }

薪资模块有一个非常经典的坑,就是小数精度问题。如果直接用double类型存金额,会出现“0.1+0.2=0.30000000000000004”这样的情况,这在财务系统里是绝对不允许的。我在源码里所有涉及金额的字段都用BigDecimal,运算时使用BigDecimal.valueOf()和.multiply()这样的方法,严谨一点还可以设置一个工具类统一处理金额运算。你只要记住一句话:涉及钱,永远用BigDecimal,不要用double和float。

4. 前端核心实现:Vue是怎么把数据渲染成页面的

4.1 前端项目目录结构怎么组织

拿到前端源码第一步,先看src目录下的结构。我的组织方式是:api文件夹放所有和后端交互的JS文件,router文件夹放路由配置,store文件夹放Vuex状态管理,views文件夹放页面级组件,components文件夹放通用组件。

这里重点说api文件夹。我习惯每个模块建一个独立的JS文件,员工模块对应employee.js,里面封装了和后端接口一一对应的函数。比如:

// api/employee.js import request from '@/utils/request' export function getEmployeeList(params) { return request({ url: '/api/employee/list', method: 'get', params }) } export function addEmployee(data) { return request({ url: '/api/employee', method: 'post', data }) }

这样做的价值在于:页面组件里不需要出现任何URL字符串,全部集中管理。后端接口路径变了,你只需要改这一个文件,不用担心全局搜索不到。

4.2 axios封装与Token携带

前端和后台打交道全靠axios,但直接用原生axios会有一个麻烦:每次请求都要手动加上Token。而且如果后端返回401状态码(Token过期),你还要在每个页面单独处理跳转登录。所以我在源码里做了一层统一封装,写在utils/request.js里面。

关键在于请求拦截器和响应拦截器的配置。请求拦截器在发出请求前,从Vuex或者localStorage里取出Token,放到header里:

request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config })

响应拦截器则统一接收后端返回的数据结构。后端协议我定义的是Result结构,包含code、message、data三个字段,code为200表示成功,为401表示未登录或者登录过期。拦截器发现code是401时,直接跳回登录页,并弹出消息提示,避免每个页面写重复代码。这就是工程化思维的体现。

4.3 员工管理页面实操:表格、弹窗、表单三件套

员工列表页是Vue最经典的“三件套”组合:一个搜索区,一个数据表格,一个弹窗表单。我拿这个页面给你详细拆解一下Vue组件的实现思路。

搜索区绑定一个searchForm对象,里面放name和deptId两个字段。点击搜索按钮时,调用后端接口传入这些参数。数据表格用Element-UI的el-table组件,绑定:data="tableData"。注意,每个操作按钮如果权限要求是可配的,可以加上v-permission自定义指令来控制显示隐藏。

弹窗表单是新增和编辑共用的同一个组件。关键技巧是:新增和编辑的区别只在于是否传了id。弹窗打开时,如果有id就调详情接口把当前记录数据回显到表单里,没id就是空白表单。保存时根据是否有id来决定调新增接口还是更新接口。这样一套代码干两件事,代码量直接减半:

handleEdit(row) { this.dialogVisible = true this.form = { ...row } // 把这一行数据拷贝到表单 }, handleAdd() { this.dialogVisible = true this.form = {} // 空表单 }

表单保存前记得做校验。我用Element-UI自带的rules校验规则,比如姓名必填、手机号正则校验,这些规则是前端拦截用户输入的第一道防线。虽然后端也有校验,但前端校验能给用户更好的体验,不用等请求发出去再提示“格式错误”。

4.4 路由守卫与页面访问控制

vue-router默认情况下是可以直接访问任意页面URL的。但对一个管理系统来说,如果用户没登录直接输入网址访问首页,应该被拦回登录页。这就是路由守卫要做的事。

我在router/index.js里配置了全局前置守卫:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path === '/login') { next() } else { if (token) { next() } else { next('/login') } } })

更进一步,还可以在后端返回的菜单数据里控制左侧边栏菜单的渲染。也就是说,同一个前端代码,不同角色登录后看到的菜单项不一样。HR专员登录后只看到员工管理和考勤管理,看不到系统管理。这个功能是用动态路由实现的,稍微复杂一些,但做出来后在答辩时讲给老师听,老师会觉得你考虑得很周全。

5. 本地部署实操:从零到能看到登录页

5.1 环境准备清单

在你准备把项目跑起来之前,先把环境检查一遍。这套项目在如下环境验证过:JDK 1.8(建议用8,兼容性最好)、Maven 3.6以上、Node.js 14到16(如果你用的Vue2,Node版本太高会碰到各种奇怪问题)、MySQL 5.7或8.0均可。

注意:这里有个大坑。如果你本机装的是Node 18、甚至Node 20,安装Vue2项目依赖的时候非常容易报错,因为node-sass这个库需要针对不同Node版本编译。解决方案是用Node 14,或者把package.json里的node-sass改成sass(dart-sass),然后重新npm install。

后端准备工作:用IDEA打开后端工程,等待Maven下载完成依赖。然后把application.yml里的数据库用户名和密码改成你自己的。创建数据库后,导入项目里附带的hrms.sql文件,这个文件里包含了全部建表语句和初始数据(默认管理员账号admin/admin123)。

前端准备工作:用VS Code打开前端工程,在终端执行npm install。如果你在国内,这一步建议先执行一下,给npm换成淘宝镜像源:

npm config set registry https://registry.npmmirror.com npm install

5.2 启动步骤与验证方式

第一步启动后端。在IDEA里找到Application启动类,右键运行。看到类似“Started Application in xxx seconds”的日志输出,说明启动成功。为了确认,可以在浏览器访问http://localhost:8080/api/system/test,如果返回一个JSON字符串,说明后端接口已经通了。

第二步启动前端。在前端目录下执行npm run serve,看到“Compiled successfully”并且终端提示运行在http://localhost:8081,就说明前端也起来了。这里有个常见情况:后端的端口和前端端口不能冲突。我把后端设为8080,前端设为8081,这样正好。

第三步验证登录。访问http://localhost:8081,输入管理员账号密码,如果能看到左侧菜单栏和主页面数据,恭喜你,整个项目已经跑通了。后续开发的时候,每改完一个接口或者页面,都不用重启服务,后端的SpringBoot DevTools和后端的Vue热更新会自动生效,能明显提升开发调试效率。

5.3 打包部署:怎么把项目变成可运行的产品

课设交付的时候,除了给源码,最好还能给一个演示环境。后端打包很简单,在项目根目录执行mvn clean package -DskipTests,会在target目录下生成一个jar包。运行这个jar包,用java -jar hrms-0.0.1.jar,后端的服务就起来了。如果是部署到服务器上,记得把数据库连接配置改成服务器上的MySQL地址。

前端打包命令是npm run build,打包完成后会生成一个dist目录。当你开发完、打完包之后,如果你想直接用一个独立的web服务器来跑前端,可以把dist目录里的文件交给nginx或者部署到IIS等静态服务器。但如果只是课设演示自己在本地看效果,其实不必非要包一层web服务器,在项目开发环境用node内置服务运行是最省事的。

6. 常见问题与排查技巧实录

6.1 项目启动报错速查表

我把实际带学生过程中常见的启动报错问题整理成一个表,给你对照着查:

报错现象根本原因解决方式
启动时提示数据库连接refusedMySQL服务没启动,或密码配置不对检查MySQL服务,检查application.yml中密码
前端npm install报node-sass错误Node版本过高不兼容降级到Node 14,或把node-sass替换为sass
后端启动提示端口被占用8080端口被其他程序占用关掉占用程序,或改application.yml中的server.port
启动后访问接口返回404后端Controller的路径没匹配上检查类上@RequestMapping和方法的路径
Mapper注入失败页面报错mapper接口没被扫描到启动类加上@MapperScan("你的mapper包路径")
登录提示用户名或密码错误密码加密后无法复现删除该用户重新新增,用BCrypt加密密码

6.2 前后端联调时几个容易踩的坑

前后端联调是课设阶段最常见的“翻车”现场。第一个坑是跨域问题。浏览器会拦截不同端口之间的AJAX请求。Vue前端在8081端口,后端在8080端口,直接请求会被CORS拦住。解决方式有两种:在后端配置CorsFilter,或者在Vue的devServer里配置proxy代理。我用的是比较省事的后端全局CORS配置,代码写在config里,一个类搞定。

第二个坑是日期格式问题。后端返回的日期是"2024-05-06T10:30:00"这种格式,前端el-date-picker组件显示出来是英文格式或者直接显示NaN。我推荐在全局统一配置Jackson的日期序列化方式,让后端统一返回"yyyy-MM-dd HH:mm:ss"格式的字符串。前端拿到字符串直接展示,不自己处理。

第三个坑是接口返回结果不能直接当数据用。你必须先养成习惯:通过控制台看完整返回的结构,找到真正数据的字段名,再把它通过赋值处理到前端变量。我在前端实现时每一步都做了严格的数据解包处理,避免错误访问undefined属性,不然页面上展示出来就是空白的。

6.3 答辩前一定要理解透的3个关键点

这也是我个人给课设学生最常说的一点。做课设,不只是代码能跑起来就万事大吉。答辩的时候,老师一定会问几个问题。第一个就是“你这个项目的权限控制是怎么实现的”。如果你答不上来,等于白做。所以,JWT和RBAC这两个概念要背熟、理解透。

第二个问题是“你遇到过什么难点,是怎么解决的”。这时候一定不要说“好像没什么难点”,要主动拿出我们上面讲的某一个坑,比如BigDecimal精度问题、前端格式显示异常等,把解决思路讲清楚,老师会立刻给你加分。

第三个问题是“如果让你扩展一个功能,你怎么做”。这是最考验系统设计能力的问题。我的建议是:准备一个自己能讲半小时的扩展方案。比如“我想给考勤模块加上请假审批流程,涉及新建请假表、审批记录表,还要加一个审批页面,用状态机控制流转状态”。能聊出这么具体的内容,说明你真的对这个系统理解到位了。

7. 个人经验与下一步可以怎么玩

最后再分享一点自己的体会。SpringBoot+Vue做毕业设计,其实不在于功能有多炫,而在于你有没有把整个开发流程吃透。我在实际调这个项目的过程中,最深的感觉是:当你把后端接口写好、前端页面联调通过、数据在页面上正常渲染出来的那个瞬间,之前踩过的所有坑都值得了。

如果你拿到这套源码,我建议不要只满足于“能跑通”。可以试着改一改:给员工信息加上文件上传(比如头像),给考勤模块加一个地图打卡(用高德地图API),或者把系统管理里的菜单管理改成动态权限加载。这些改造的每一步,都是在为你自己的技术能力添砖加瓦。

等到答辩演示的时候,你可以很坦然地说:这个系统从数据库设计到前后端联调,每一个环节我都亲手实现过。老师如果再追问点什么,你也能接得住。这套源码就是你的一个完整的练手项目,把它吃透,你的实践能力会比只看视频刷题扎实得多。

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

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

立即咨询