资讯动态

Spring Boot + Vue打造人力资源管理系统:从数据库设计到权限控制

发布时间:2026/10/14 21:49:10 来源:尧图企业网站定制
1. 项目整体设计与思路拆解1.1 为什么还要做一套人力资源管理系统很多同学在选毕业设计课题或者练手项目时第一反应是“HR系统是不是做烂了”。这话不假市面上开源的人力资源管理系统一把一把的但恰恰因为烂大街反而说明这个题目有它的合理之处需求足够经典、业务边界足够清晰、技术栈足够匹配企业真实开发场景。我见过太多人上来就选什么“基于微服务的电商平台”“基于区块链的溯源系统”结果做了一半发现能力跟不上代码全是CtrlC和CtrlV答辩的时候连自己项目里的核心流程都讲不清楚。人力资源管理系统恰好卡在一个非常适合全栈练手的复杂度区间。你说它简单它有员工信息管理、组织架构管理、考勤排班、薪资计算、招聘流程、权限控制这些业务模块逻辑链条完整你说它复杂它又不像电商系统那样需要处理高并发秒杀、分布式事务、消息队列削峰这些高难度问题。换句话说这个选题的技术挑战核心不在“高并发”而在“业务建模的清晰度”和“前后端协作的规范度”这两个能力恰恰是刚入行的开发者最需要建立的职业素养。从实际复用价值来看这套系统做完之后可以改造成企业内部的管理后台、社团管理平台、甚至是一套通用的CRUD权限脚手架。我见过不少开发者把自己做的HR系统经过简单改造直接变成了公司的内部后台基座因为员工管理、角色权限、操作日志这些模块几乎是所有管理系统的公共底座。1.2 技术选型背后的真实考量这次项目采用的技术栈是Spring Boot Vue ElementUI后端2.x版本搭配前端2.x版本看起来是市面上最常见的组合但每个技术选型背后都有实际的理由。Spring Boot作为后端框架最大的价值不是它比SSH省了多少XML配置而是它带来了“约定大于配置”的开发节奏。对于一个人要写完前后端的场景来说Spring Boot能让你把注意力集中在业务代码本身而不是耗费大量时间在框架配置上。配合MyBatis Plus使用单表CRUD几乎不需要手写SQL连分页都帮你封装好了。如果这个项目的目标是快速成型、稳定运行这个组合是最不容易出意外的。Vue 2 ElementUI的组合放在今天看来不算最新但胜在生态极其成熟。ElementUI的表格组件、表单组件、弹窗组件、树形控件直接覆盖了HR系统的全部界面需求你不需要花一个下午去研究某个组件怎么实现“编辑弹窗表单回显”这种功能人家早就帮你做好了。而且网上针对这两个框架的资料多到溢出遇到任何问题都能快速找到解决方案。这套组合还有一个隐含的优势团队协作成本低。如果后续有同学加入项目Vue 2的学习曲线比Vue 3组合式API要缓和不少沟通成本低上手速度快。我没有选择把后端拆分成微服务也没有选择用Redis做缓存、用RabbitMQ做异步消息。原因很简单HR系统本身的并发量并不高一个几百人规模的企业每日活跃操作撑死几千次单体架构完全扛得住。引入微服务和消息队列只会增加部署复杂度对系统性能的提升却微乎其微属于典型的技术过度设计。我在实际开发中一条原则就是系统复杂度必须匹配业务复杂度宁可留出扩展空间也不要为了技术而技术。1.3 模块规划与功能边界划分整个系统我规划为六大核心功能模块外加一个系统管理模块作为公共底座员工管理模块员工信息档案、入职登记、离职办理、员工调动记录组织管理模块部门结构树、岗位信息、编制管理考勤管理模块排班管理、打卡记录、请假审批、加班登记薪资管理模块薪资结构设置、工资条生成、社保公积金计算招聘管理模块职位发布、简历管理、面试流程跟踪系统管理模块用户管理、角色管理、菜单权限、操作日志每个模块都不是孤立的。员工档案会关联到部门树上的节点考勤数据会汇总进入薪资计算流程招聘通过审批的员工需要一键转入员工管理模块系统管理中的角色菜单权限决定每个用户能看到的操作按钮。这些模块之间的数据流转关系构成了整个系统最核心的业务骨架。在功能边界划分上我刻意没有把“培训管理”“绩效管理”塞进第一版。原因很简单——这两块业务弹性太大需求定义稍有不清晰就会导致数据库设计反复返工。绩效管理的考核指标体系在不同的企业可能是完全不同的两套玩法培训管理的课程体系、签到统计、效果评估做起来工作量也不小。做项目最忌讳的是一开始就把范围铺太大先把核心闭环打通再逐步迭代扩展这才是做项目的基本节奏。2. 数据库设计与权限模型构建2.1 数据库表结构设计思路数据库设计是整个系统的地基这块如果做得烂后面写业务代码的时候会处处难受。我这次采用的是MySQL 5.7数据库InnoDB存储引擎utf8mb4字符集。表结构设计遵循了三范式但在具体字段的设计上做了适度冗余目的很简单减少多表联查的频率提升查询性能。核心数据表分为几组第一组是组织架构相关的表部门表和岗位表。部门表的核心字段包括部门ID、部门名称、父部门ID、负责人ID、排序号、状态。这里有个关键设计——通过父部门ID字段实现树形结构而不是单独建一张层级关系表。对于HR系统这个业务场景来说部门层级一般不会超过四层直接用parent_id字段维护树形关系就足够了查询子部门时递归查询逻辑简单直观。第二组是员工相关的主数据和从表。员工主表存储工号、姓名、性别、出生日期、身份证号、手机号、邮箱、入职日期、员工状态、部门ID、岗位ID等核心字段。这里特别要注意的是员工状态字段我设置了试用期、正式、离职、停薪留职四种状态后续很多业务流程都会根据这个字段做条件过滤。除了主表之外还单独建了员工教育经历表和员工工作经历表都属于典型的1对N从表设计。第三组是考勤和薪资相关的业务表。考勤记录表是最核心的一张表字段包括员工ID、考勤日期、上班打卡时间、下班打卡时间、考勤状态正常、迟到、早退、缺勤、请假、加班。薪资表的设计要复杂一些分为薪资结构表和工资条明细表。薪资结构表定义每个岗位的基本工资和岗位工资工资条明细表则是每个月实际生成的工资单记录实际发放的各项金额。第四组就是系统管理相关的表用户表、角色表、菜单表、用户角色关联表、角色菜单关联表、操作日志表。用户表在员工表是外键关联的但不是强制一对一绑定。我在这里做的设计是用户表独立存在user_id关联员工表emp_id允许为空。这样设计的好处是系统可以存在部分“非员工用户”比如系统维护人员、外部审计人员等灵活度更高。2.2 核心表结构的关键SQL参考部门表结构设计如下CREATE TABLE sys_dept ( dept_id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 部门ID, parent_id bigint(20) DEFAULT 0 COMMENT 父部门ID, ancestors varchar(50) DEFAULT COMMENT 祖级列表, dept_name varchar(50) NOT NULL COMMENT 部门名称, order_num int(4) DEFAULT 0 COMMENT 显示顺序, leader varchar(20) DEFAULT NULL COMMENT 负责人, phone varchar(11) DEFAULT NULL COMMENT 联系电话, email varchar(50) DEFAULT NULL COMMENT 邮箱, status char(1) DEFAULT 0 COMMENT 部门状态0正常 1停用, del_flag char(1) DEFAULT 0 COMMENT 删除标志0存在 2删除, create_by varchar(64) DEFAULT COMMENT 创建者, create_time datetime DEFAULT NULL COMMENT 创建时间, update_by varchar(64) DEFAULT COMMENT 更新者, update_time datetime DEFAULT NULL COMMENT 更新时间, PRIMARY KEY (dept_id) ) ENGINEInnoDB AUTO_INCREMENT1 COMMENT部门表;员工主表是整张系统中数据量最大、字段最多的表这里给出一个精简后的核心版本CREATE TABLE emp_employee ( emp_id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 员工ID, emp_no varchar(20) NOT NULL COMMENT 工号, emp_name varchar(30) NOT NULL COMMENT 姓名, gender char(1) DEFAULT 0 COMMENT 性别0男 1女, birth_date date DEFAULT NULL COMMENT 出生日期, id_card varchar(18) DEFAULT NULL COMMENT 身份证号, phone varchar(11) DEFAULT NULL COMMENT 手机号, email varchar(50) DEFAULT NULL COMMENT 邮箱, dept_id bigint(20) DEFAULT NULL COMMENT 部门ID, post_id bigint(20) DEFAULT NULL COMMENT 岗位ID, education varchar(20) DEFAULT NULL COMMENT 学历, entry_date date DEFAULT NULL COMMENT 入职日期, emp_status char(1) DEFAULT 0 COMMENT 员工状态0试用期 1正式 2离职 3停薪留职, create_by varchar(64) DEFAULT COMMENT 创建者, create_time datetime DEFAULT NULL COMMENT 创建时间, update_by varchar(64) DEFAULT COMMENT 更新者, update_time datetime DEFAULT NULL COMMENT 更新时间, PRIMARY KEY (emp_id) ) ENGINEInnoDB AUTO_INCREMENT1 COMMENT员工信息表;关于表结构的经验之谈一定要给所有业务表都加上create_by、create_time、update_by、update_time这四个公共审计字段。很多刚入行的开发者在设计数据库时容易忽略这一点结果系统运行一段时间后出现问题想追溯数据来源发现完全没有线索只能干瞪眼。加上审计字段对业务逻辑没有任何负担但对后期的数据维护和问题排查帮助巨大。del_flag这个逻辑删除标志位也值得展开说。我在所有主表上都设计了逻辑删除而不是物理删除。因为HR系统涉及的数据带有很强的历史追溯属性员工离职后他的档案不能直接删掉考勤记录更是不能丢。逻辑删除的好处是数据不真正消失只是查询条件默认过滤掉一旦需要审计或者追溯历史数据仍然完好存在。2.3 权限模型与RBAC设计权限模型采用的是经典RBAC基于角色的访问控制模型拆成五张核心表用户表、角色表、菜单表、用户角色关联表、角色菜单关联表。用户在系统中的权限能力完全由“用户-角色-菜单”这条链路决定。菜单表中有一类比较特殊的数据——按钮权限。我在设计时把页面上每个操作按钮都纳入权限控制范围比如“新增员工”按钮、“删除员工”按钮、“导出工资条”按钮。每个按钮对应菜单表里的一条记录type字段标识是目录、菜单还是按钮。这样前端可以根据用户拥有的按钮权限动态控制页面上的按钮渲染。没有“删除员工”权限的用户界面上根本不出现删除按钮而不是出现按钮后点击时后端返回错误。登录认证这块我用的是Spring Security JWT的方案。登录成功后由后端生成一个JWT令牌前端通过Axios拦截器把令牌Token附加到每个请求的请求头中后端过滤器对请求进行校验。选择JWT而不是传统Session方案最核心的原因是前端和后端完全分离部署JWT的无状态特性天然适合这种架构。这里有一个需要注意的坑JWT令牌是无状态的服务端没法主动让某个令牌失效。如果用户的角色权限在令牌有效期内发生了变化旧令牌仍然能访问旧权限范围内的接口直到令牌过期。解决方式有两种一种是把令牌有效期设短一点比如30分钟需要刷新机制配合另一种是在Redis中维护一张令牌黑名单或者权限版本号。我这次采用的是把令牌有效期设为2小时同时提供刷新令牌的接口权限变更的频率在这个场景下远低于2小时一次问题不突出。2.4 数据库设计的三个避坑建议第一字段类型选择上金额字段一律使用DECIMAL而不是FLOAT或DOUBLE。这个教训我是在薪资计算模块里吃过的。浮点数在计算机中本身就是近似值员工工资这种东西精确到分累计计算几次之后误差就会累加出来。DECIMAL(10,2)是金额字段的标准配置。第二手机号、身份证号这类字段在数据库里建议按字符串存储而不是数值类型。很多新手喜欢把手机号设置成bigint结果遇到手机号前几位是0的情况数据直接丢失。身份证号更不用说18位数字在bigint范围内能存下但未来如果涉及X结尾的号码数值类型就完全存不了。第三简单查询场景不要过度设计索引。我给employee表的emp_no字段建了唯一索引给dept_id、post_id、emp_status这些高频查询字段建了普通索引。但其他的组合索引一律没建因为系统的数据量也就是千级别索引建多了反而拖慢写入速度、浪费存储空间属于典型的画蛇添足。3. 后端核心功能实现与技术要点3.1 Spring Boot项目工程结构后端的项目结构按功能模块分包不是按技术层次分包。这个组织方式的区别很重要。按技术层次分包是controller、service、mapper各建一个包所有业务模块的Controller全部塞在一起。按功能分包则是employee、attendance、salary各建一个顶层包每个包下面再分出controller、service、mapper子包。我这次使用的是按功能分包的方式结构大概是这样的com.company.hrms ├── common // 通用工具类、常量、异常处理 │ ├── constant │ ├── exception │ └── utils ├── config // 配置类Security配置、MyBatisPlus配置、跨域配置 ├── system // 系统管理模块用户、角色、菜单 │ ├── controller │ ├── service │ └── mapper ├── employee // 员工管理模块 │ ├── controller │ ├── service │ ├── mapper │ └── domain ├── attendance // 考勤管理模块 ├── salary // 薪资管理模块 └── recruit // 招聘管理模块这种组织方式在功能模块越来越多时优势会非常明显。一个模块的功能涉及到的代码文件彼此靠近改起来不需要在十几个包之间来回跳转代码审查时按模块审查也很清晰。后端统一返回结果类设计为Result对象包含code、msg、data三个字段所有接口都返回这个统一格式。前端Axios响应拦截器统一判断code值成功才进入业务逻辑失败时直接弹出错误提示。这个规范看着不起眼但对前后端协作效率的提升是巨大的沟通成本直线下降——不需要每次联调都去问“这个接口失败时返回什么格式”所有接口都是一个约定。3.2 员工管理模块的核心业务逻辑员工管理模块是HR系统的“主数据”模块其他模块都在围绕它转这个模块我做的是最扎实的。员工信息的CRUD接口中复杂度最高的不是增删改查本身而是工号生成逻辑和员工状态流转。工号生成规则是“部门编码入职年份流水号”比如部门编码是D04入职年份是2025流水号从001递增生成结果就是D042025001。这个逻辑必须在后端生成不能用前端传值。我通过查询当前部门当年已有员工数再加一作为流水号同时为了保证并发下工号不重复在数据库层面对emp_no加了唯一索引双重保险。员工状态流转这块有几个约束试用期员工只能转正式、办理离职或停薪留职正式员工可以转停薪留职或办理离职离职员工不能重新变回正式状态只能走“重新入职”流程。这些状态流转规则我用一个独立的状态机类维护不在Service层散写代码判断。状态机的优势是新增一种状态时只需修改状态机定义不会遗漏某个分支逻辑。员工查询接口支持多条件组合筛选员工姓名模糊查询、部门精确查询、状态精确查询、入职日期范围查询。这个接口的分页查询使用MyBatis Plus的LambdaQueryWrapper实现条件构造没有手写XML SQL。MyBatis Plus在这个场景下效率确实高但如果查询逻辑特别复杂、涉及多表嵌套查询还是建议写XML SQL更可控。我的经验是单表条件查询用Wrapper多表关联查询用XML自定义SQL两条线并行互不干扰。删除员工的逻辑需要注意这里我实现的是逻辑删除而不是物理删除主要原因在上一节已经详细说明了。但逻辑删除有个连锁问题需要处理——员工关联的考勤记录、工资记录、合同记录都需要一并做逻辑删除或者置为失效状态保持数据一致性。3.3 考勤与薪资模块的算法设计考勤模块的核心是考勤状态判定算法。我的实现思路是员工在考勤规则表里维护一个排班计划比如工作日9点上班、18点下班。打卡记录表保存每天的打点记录系统在每晚定时任务中根据当天上班打卡时间和排班计划进行比对自动生成考勤状态上班打卡时间不超过规定时间9点整的判定为正常打卡时间超过9点但在9点30分之前的判定为迟到超过9点30分判定为缺勤同样的逻辑应用在下班时间。这里要处理的一个边界场景是跨天排班比如晚班是晚上22点到第二天早上6点。排班计划的字段设计上要能表达这种跨天场景我通过在排班计划表中增加shift_type字段来标识早晚班类型考勤判定逻辑里对晚班和早班采用不同的比对逻辑。自动生成考勤状态我建议用定时任务实现Spring Boot中自带的Scheduled注解就够用了。需要注意的是定时任务逻辑要写成幂等的——重复执行不会产生重复数据。做法是在考勤记录表中对“员工ID考勤日期”建唯一索引插入时如果冲突则进行更新操作而不是重复插入。薪资计算模块是最容易出错的部分。工资条的计算公式为实发工资 基本工资 岗位工资 绩效奖金 加班费 - 应扣社保 - 应扣公积金 - 个人所得税社保和公积金的计算基准是缴费基数这个基数的确定规则通常是取员工上年度月平均工资但这个值有上下限限制我设计时在薪资结构表中维护了一个缴费基数上下限的配置项避免算出来的社保金额不合理。个人所得税的计算使用最新的累进税率表这一块涉及的数据比较敏感计算逻辑一定要写单元测试覆盖各档税率边界值。工资条生成后往工资条明细表中写入数据同时可以生成PDF文件推送给员工的账户。这里的推送功能不是核心需求我采用了简单的邮件方式发送PDF附件通过JavaMailSender实现。这套流程跑通之后从月初考勤统计到工资条发放的自动化闭环就完全建成了。3.4 系统管理与操作日志设计系统管理模块中的登录功能用了Spring Security框架。登录接口不做自定义Controller处理而是通过Spring Security的过滤器链机制实现Token的签发和校验。用户输入用户名密码认证管理器校验通过后创建JWT令牌返回。权限控制的核心是PreAuthorize注解。在每个需要权限的Controller接口方法上标注权限标识符比如PreAuthorize(ss.hasPermission(employee:add)) PostMapping(/employee) public Result addEmployee(RequestBody EmployeeDTO dto) { // 业务逻辑 }ss.hasPermission方法会从当前登录用户的权限列表中查询是否包含指定的权限标识。这个权限标识在菜单表中配置对应页面上按钮的权限标记实现前后端权限控制的统一。操作日志模块使用Spring AOP实现自定义了一个Log注解。在核心业务接口上标注该注解后AOP切面会在接口执行前后自动记录操作信息。日志内容包含操作人、操作模块、操作类型、请求参数、请求耗时、IP地址、操作结果。参数序列化时要做到脱敏处理比如身份证号和手机号字段在日志中做掩码显示这是数据安全合规的基本要求。3.5 关键接口一览与请求响应示例后端接口设计遵循RESTful风格以下是部分核心接口的约定接口地址请求方式功能说明权限标识/api/employee/pageGET分页查询员工列表employee:list/api/employee/{id}GET查询员工详情employee:query/api/employeePOST新增员工employee:add/api/employeePUT修改员工信息employee:edit/api/employee/{id}DELETE删除员工employee:remove/api/attendance/record/pageGET分页查询考勤记录attendance:list/api/attendance/record/generatePOST手动触发考勤记录生成attendance:generate/api/salary/detail/pageGET分页查询工资条salary:list/api/salary/calculatePOST触发薪资计算salary:calculate/api/system/loginPOST用户登录无/api/system/logoutPOST退出登录无接口返回统一结果格式的JSON示例{ code: 200, msg: 操作成功, data: { records: [ { empId: 1001, empNo: D042025001, empName: 张三, deptName: 技术部, postName: Java开发工程师, empStatus: 1, entryDate: 2025-03-15 } ], total: 26, size: 10, current: 1 } }4. 前端页面实现与前后端联调4.1 Vue项目的工程化搭建前端项目使用Vue CLI脚手架创建先配置好基础依赖Vue Router负责前端路由Vuex负责全局状态管理Axios负责HTTP请求ElementUI负责UI组件。这套工具链是Vue 2时代最标准的全家桶组合。前端工程结构分为几个核心目录src ├── api // 接口请求封装 │ ├── employee.js │ ├── attendance.js │ ├── salary.js │ └── system.js ├── assets // 静态资源 ├── components // 公共组件 ├── router // 路由配置 ├── store // Vuex状态 ├── utils // 工具函数request封装auth工具 ├── views // 页面视图 │ ├── employee │ ├── attendance │ ├── salary │ ├── recruit │ └── system └── main.jsapi目录下的每个文件对应一个后端模块里面是细分到每个接口的请求函数。比如employee.js中会定义getEmployeePage、addEmployee、updateEmployee、deleteEmployee、getEmployeeDetail五个函数。view页面中不直接调用Axios而是通过调用这些api函数发起请求好处是页面代码中看不到任何请求URL字符串修改接口地址只需改一处。路由配置需要配合后端返回的菜单权限动态生成。用户登录成功后系统根据用户的角色权限动态生成可访问的路由表然后通过router.addRoutes方法添加到前端路由中。这样做的好处是无权限的页面即使通过URL强制访问也会被前端路由拦截搭配后端接口权限校验形成双重防护。4.2 ElementUI核心组件的实战用法前端页面中最常用的ElementUI组件是表格组件、表单组件、弹窗组件、树形组件、分页组件。这里挑几个最关键的实战用法来说。员工列表页是最典型的“搜索区表格区分页区”三段式布局。搜索区是表单组件表格区是Table组件分页区是Pagination组件。搜索条件变动时重新请求第一页数据分页信息变化时重新请求对应页的数据。表格操作列中根据用户的按钮权限渲染编辑和删除按钮权限判断通过Vuex中存储的用户按钮标识列表组合完成。表单弹窗是员工新增和编辑共用的一个弹窗组件通过传入的formData对象区分是新增还是编辑模式。表单校验规则在el-form-item上通过rules属性声明对必填字段、手机号格式、邮箱格式等进行前端校验。这里有一个经验之谈——前端校验只能提升用户体验真正防非法数据必须依赖后端校验。我在后端对每个DTO对象都加了NotNull、Pattern等校验注解前端传过来的数据在后端Controller入口就被拦截掉了根本不会进入业务逻辑层。部门树组件在员工管理页的左侧栏使用点击树节点时把当前部门ID传入列表查询条件实现按部门筛选员工的功能。树的节点数据从后端部门接口异步加载一次加载全部部门数据后在内存中构建树因为部门数据量本身不大不需要做懒加载优化。4.3 Vuex状态管理与登录状态保持Vuex中主要存储三类全局状态用户信息、Token令牌、权限标识列表。登录成功后把这三类数据写入Vuex同时存到LocalStorage中浏览器页面刷新后从LocalStorage恢复Vuex状态保证刷新页面不会丢失登录态。Axios封装的核心在utils/request.js中这里实现了两个关键的拦截器逻辑请求拦截器从LocalStorage中取出Token加到请求头的Authorization字段上。如果Token不存在直接跳转登录页。响应拦截器判断HTTP状态码和后端Result对象中的code值。code值等于401时说明Token过期清除本地登录态并跳转登录页。code值为200时进入正常业务逻辑其他code值弹出错误提示框。这里有一个排坑经验要分享后端接口在处理业务异常时不要返回HTTP 500状态码而是返回HTTP 200加上业务错误码。这样前后端联调时HTTP层的错误和业务层的错误可以清晰区分Axios拦截器处理起来逻辑也简单很多。自定义业务异常统一抛出BusinessException由全局异常处理器转换为对应的Result格式。4.4 前后端联调的关键配置前后端分离开发时最常遇到的问题是跨域请求。开发环境下Vue项目运行在8080端口后端运行在8081端口前端请求后端接口必然存在跨域问题。解决方式有两种后端配置CORS跨域规则或者前端通过Vue CLI的devServer配置代理。我这次使用的是前端代理方案在vue.config.js中配置module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } } }这样配置之后前端请求/api/employee/page时开发服务器会把请求转发到http://localhost:8081/api/employee/page浏览器的请求是发给同源的开发服务器不存在跨域问题。生产部署时再用Nginx做反向代理统一转发。这个方案比后端配置CORS更优雅同时避免了预检请求带来的额外网络开销。Axios请求超时时间建议设置为15秒太短容易误报超时太长则交互卡顿体验不好。我同时在后端设置了一个全局的接口响应时间监控日志打印出每个接口的处理耗时方便定位慢接口。4.5 前端页面核心代码参考员工列表页模板的核心结构长这样template div classapp-container !-- 搜索区 -- el-form :modelqueryParams inline el-form-item label员工姓名 el-input v-modelqueryParams.empName placeholder请输入员工姓名 clearable / /el-form-item el-form-item label部门 el-select v-modelqueryParams.deptId placeholder请选择部门 clearable el-option v-fordept in deptOptions :keydept.deptId :labeldept.deptName :valuedept.deptId / /el-select /el-form-item el-form-item el-button typeprimary clickhandleQuery搜索/el-button el-button clickresetQuery重置/el-button /el-form-item /el-form !-- 表格区 -- el-table :datatableData v-loadingloading el-table-column propempNo label工号 / el-table-column propempName label姓名 / el-table-column propdeptName label部门 / el-table-column proppostName label岗位 / el-table-column propphone label手机号 / el-table-column label操作 width240 template slot-scopescope el-button typetext clickhandleEdit(scope.row)编辑/el-button el-button typetext clickhandleDelete(scope.row)删除/el-button /template /el-table-column /el-table !-- 分页区 -- el-pagination v-showtotal 0 :totaltotal :current-page.syncqueryParams.pageNum :page-size.syncqueryParams.pageSize :page-sizes[10, 20, 30, 50] layouttotal, sizes, prev, pager, next, jumper size-changehandleQuery current-changehandleQuery / /div /template这段代码看起来简单实际开发时有两个容易踩坑的点。第一是分页组件的current-page.sync和page-size.sync修饰符这要求queryParams对象中的对应字段必须初始化好否则首次渲染时组件会警告。第二是搜索区的表单数据对象queryParams必须包含分页参数pageNum和pageSize因为handleQuery方法会把整个queryParams作为查询参数传给后端接口。5. 系统测试与性能调优实录5.1 功能测试要点与测试用例设计系统测试阶段我采用了“功能测试优先、性能测试兜底”的策略。功能测试覆盖核心业务闭环流程具体包括员工入职测试场景涉及新增员工、分配部门岗位、创建系统账号、分配角色权限四个环节。测试数据准备两个边界情况工号已有但姓名不同的员工系统给出提示手机号重复的员工系统给出格式校验。都把边界数据跑了一遍确认不会产生脏数据。请假审批流程测试走的是员工提交请假、直属主管审批、考勤管理员备案三个环节。测试中重点验证不同角色登录后看到的操作按钮是否正确——员工只能提交请假单主管只能审批本部门员工的请假单考勤管理员能查看全部备案记录。角色权限的判定逻辑也是在这轮测试中修了不少Bug。薪资计算测试是最需要细心的一环。计算逻辑设计为可配置测试时分别跑了五组不同缴费基数、不同入离职月份的员工数据和Excel手工计算的结果逐一核对。第一轮测试跑出了误差排查后发现是社保上限配置没有生效修完之后第二轮测试结果完全一致。薪资这类钱相关的模块绝对不能“差不多就行”必须数字精确一分不差。5.2 性能测试中发现的问题与优化系统并发量不高但我还是用JMeter简单跑了一轮性能测试模拟30个并发用户连续操作5分钟主要关注接口响应时间和错误率。测试发现最慢的接口是员工列表查询接口平均响应时间在800毫秒左右。原因是员工查询接口走了逻辑删除过滤条件加多个查询条件组合SQL的WHERE条件过多导致索引未能完全命中。优化方案是重新分析查询字段的组合情况把“部门ID员工状态”组合索引加上同时给员工姓名字段增加了前缀索引。优化后平均响应时间降到了150毫秒左右效果显著。对于一个CRUD系统来说200毫秒以内的接口响应时间属于正常水平300毫秒以上就值得关注了。性能测试的价值在于提前发现这些隐患而不是等到上线后用户来反馈“系统有点卡”。5.3 部署环境与上线流程系统部署采用前后端分离部署方案。后端的Spring Boot项目打成JAR包部署在服务器上运行前端项目通过打包命令生成dist目录下的静态文件由Nginx进行托管。后端部署时配置了多环境Profile区分开发环境、测试环境、生产环境。生产环境的数据库连接信息、Redis连接信息等敏感配置不写在代码中而是通过环境变量注入。这个习惯很重要很多项目代码泄露导致的数据安全事故根源就是配置文件中硬编码了生产环境的数据库密码。Nginx的核心配置需要注意两个点一是静态文件路径的配置把dist目录设置为站点根目录二是接口请求的反向代理配置将/api路径的请求转发到后端应用地址。server { listen 80; server_name localhost; location / { root /opt/hrms/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files配置是在做单页应用时容易遗漏的关键点。Vue Router使用history模式时用户访问某个子路由并进行页面刷新Nginx会直接查找对应路径的静态文件然而这个文件其实是不存在的。加上try_files配置后所有不存在的路径请求都会回退到index.html由前端路由接管完美解决刷新404问题。5.4 代码质量与项目规范项目代码的命名规范遵循了阿里Java开发手册的标准。类名使用大驼峰方法名使用小驼峰常量使用全大写加下划线分隔Service层接口和实现类分离。Controller层只负责参数接收和结果封装不写业务逻辑Service层是业务逻辑的主要承载者Mapper层只做数据访问。分层职责清晰后期接手的人不需要在代码之间反复横跳一眼就知道去哪找逻辑。写单元测试是一个需要坚持的好习惯。我对薪资计算、考勤状态判定这类核心算法都写了单元测试用JUnit 5实现。单元测试不是测试工作量的堆砌而是在代码修改后第一时间告诉开发人员“你没改坏东西”。薪资模块改过一次跑一遍既有测试用例发现有一条用例挂了提前就暴露了问题比上线后被财务部门发现少了几百块钱要强无数倍。6. 常见问题与排查技巧实录6.1 登录认证环节的典型问题第一个高频问题前端点击登录没有任何反应浏览器控制台显示跨域请求错误。排查思路是先用Postman直接请求后端登录接口确认后端接口本身是否正常。后端正常的话问题就出在代理配置或者CORS配置上。我实际踩过的坑是Vue CLI代理配置中的changeOrigin参数没有设置导致后端收到的请求头来源不对最终还是代理转发没有正确处理跨域。第二个高频问题登录成功后访问其他接口返回401错误。这个问题的原因通常是请求拦截器没有正确附加Token到请求头或者后端的JWT过滤器判断Token的密钥与签发Token时的密钥不一致。排查时先看浏览器存储中Token是否存在再看请求头中Authorization字段是否带上了值最后看后端日志中过滤器的校验失败原因。6.2 员工模块的权限与数据问题新增员工成功但列表页看不到该员工。这种问题要先想到逻辑删除标志位的坑。排查时发现是查询条件中filterDeleted方法用错了位置写在了某个分支条件里导致部分查询场景没有过滤已删除数据。修掉这个Bug的过程让我对MyBatis Plus的Wrapper构建逻辑更加小心了——公共过滤条件一定要放在所有查询方法都必经的同一个地方不能散落在各个条件分支里。普通用户登录后看到了全部员工列表而不是本部门员工列表。这个问题涉及数据权限控制。我设计的方案是注解DataScope配合部门ID过滤在Mapper查询SQL中自动追加部门权限条件。初次实现时注解没有加到查询方法上权限条件没有生效。修正后管理员能看到全部数据普通员工只能看到本部门及其子部门下的员工数据。6.3 考勤与薪资模块的边界数据问题考勤记录生成定时任务跑完之后发现一部分员工没有生成考勤记录。排查原因是这部分员工在排班计划表中没有维护对应的排班计划。定时任务在设计时直接跳过了无排班计划的员工导致漏数据。修改方案是定时任务对无排班计划的员工生成“提醒记录”提醒考勤管理员尽快补充排班信息这样就不会出现静默丢数据的情况。薪资计算结果与手工计算不一致差了几块钱。排查定位到是社保缴费基数的上下限配置没有考虑到最新年度的调整数据。这类边界配置的变更往往不会被及时发现建议薪资结构表中维护一个配置版本号每次调整都作为一条新记录插入计算时取最新版本同时保留历史版本可以做追溯和复盘。6.4 前端页面的渲染与交互问题表格数据正常返回但页面显示空白打开浏览器控制台发现Vue警告数据没有定义。这类问题在使用ElementUI时比较常见大部分原因是组件绑定的数据变量在data()中没有声明初始化。Vue 2的响应式系统要求必须在data函数中预先声明响应式属性。解决方案就是初始化所有需要响应式更新的数据字段。弹窗表单编辑回显时部门下拉框显示的是部门ID而不是部门名称。排坑的经验是后端返回的实体类中同时包含deptId和deptName两个字段弹窗打开时先用后端返回详情数据回显表单部门下拉框的选中逻辑要基于deptId匹配。如果只返回deptId而不返回部门名称的下拉选项数据下拉框就显示不出名字来。7. 项目扩展方向与实际心得7.1 再有时间可以继续做的扩展功能第一版系统跑通之后如果有余力可以往这几个方向扩展。报表统计方面可以把员工入离职趋势、部门人数分布、考勤异常统计这些数据做成可视化图表。后端增加几个统计接口输出聚合数据前端用ECharts做图表展示这一个扩展就能让系统从“管理工具”升级成“决策辅助工具”演示效果会好很多。消息提醒方面目前系统是纯被动响应式审批流程中的待办事项全靠用户主动刷新页面查看。引入WebSocket或者轮询机制实现在线消息通知和待办事项提醒对使用体验的提升非常明显。移动端适配方面核心的考勤打卡、请假审批这类高频功能可以单独做一个H5页面不需要完整做一套移动App用Spring Boot的REST接口直接复用后端能力前端用移动端组件库重新做几个适配页面就行。7.2 我从这个项目中沉淀的三条经验第一条经验是关于业务理解的。开始写代码之前我花了两天时间研究HR系统的业务流程弄清楚员工从入职到离职经历了哪些环节、每个环节涉及哪些部门哪些角色。这些前置研究看起来拖慢了开发进度但在动手编码之后带来了非常直接的好处——数据库设计一次成型业务模块之间的数据流打通几乎没走过回头路。做业务系统最忌讳的是一上来就建表写代码到后面发现业务理解有偏差再返工成本翻好几倍。第二条经验是关于技术选型的。面对一个具体的项目成熟的、团队熟悉度高的技术方案才是最优解。Spring Boot Vue ElementUI在2025年看起来确实不算新潮但它的生态成熟度是无可比拟的遇到问题百分之百能找到现成答案。反过来如果当时选了个刚出没多久的新框架可能连文档都还没补全遇到一个编译错误就要查半天项目节奏就全打乱了。第三条经验是关于质量意识的。系统里的每一个金额字段、每一个判定逻辑、每一个边界条件在上线之前都要经过反复验证。HR系统服务的对象是公司内部的员工一张错误计算的工资条足以让整个团队对系统的信任度清零。质量是靠测试用例、代码走查、上线前演练堆出来的不是靠口号喊出来的。这套系统做完之后我自己最大的感受是一个看似普通的CRUD项目做扎实了同样能收获巨大的成长。从数据库设计到权限控制从算法逻辑到部署上线每一个环节都是实际开发中必然面对的能力。如果你也打算动手做一套类似的系统不妨先把这个项目的核心闭环定义清楚把每个模块的业务逻辑想透再开始写第一行代码。

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

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

免费获取报价 →
↑