尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring Boot员工工资管理系统:考勤与工资核算全链路实战解析

Spring Boot员工工资管理系统:考勤与工资核算全链路实战解析 做Java毕业设计或者课程设计的同学看到员工工资管理系统这种题目应该不陌生了Spring Boot全家桶一搭CRUD一写一个看起来很完整的系统就出来了。但真正动手做过的人都知道这类系统的难点从来不是Controller和Service那几层而是工资怎么算、考勤怎么统计、这俩数据怎么联动这些背后看不见的业务逻辑。本文就从一个实际能跑通的Spring Boot员工工资管理系统出发把员工管理、考勤打卡、薪资核算这条完整链路拆开讲清楚包括数据库怎么设计、核心代码怎么组织、运行部署有哪些坑适合正在做同类毕设选题、或者想快速上手Spring Boot做管理系统的同学参考。1. 为什么工资考勤这类系统始终是Spring Boot选题里的常青树先聊个现象。每年Java方向的毕设题目几乎都会出现员工管理系统、工资管理系统、考勤管理系统这一类很多人觉得这是学校偷懒题目老掉牙。但如果你真去做过就会发现这类系统能常年存在是因为它覆盖的技术点足够聚焦、也足够典型。拿我这个项目来说表面上就是员工工资管理系统实际拆开来看里面至少包含三类核心能力一是基础数据管理员工信息的增删改查、部门维护二是业务过程数据也就是考勤打卡、请假记录、加班工时的采集三是结果数据也就是根据考勤数据、岗位工资、绩效系数这些因子按月计算出每个员工的应发工资、实发工资。这三类数据层层递进恰好对应了一个后台管理系统从简单CRUD到复杂业务计算的完整链路。为什么Spring Boot适合做这个这里必须说清楚一个关键点这套系统重的不是界面也不是并发而是业务规则的编排。工资计算的规则往往不是固定的比如不同岗位有不同的基本工资迟到扣款按分钟算还是按次算全勤奖的门槛是多少天社保公积金的扣除比例怎么配置。这些规则用Spring Boot的Service层来组织非常顺手因为Spring的依赖注入让每个业务模块可以互相独立又方便组合测试和迭代的成本都低。换成传统的JSPServlet写死逻辑改一条规则就要动一大片代码而Spring Boot天然的分层结构让这些规则变成了一个个可替换的组件。另外Spring Boot对新手友好的工程结构也是它成为毕设首选的原因。标准的三层架构Controller、Service、Mapper配合Spring Data JPA或者MyBatis几乎不需要额外配置就能跑通一条前端请求→后端接口→数据库操作的完整链路。再加上内置的Tomcat、自动化的配置管理本地起一个项目基本就是几分钟的事。对这些系统的初学者来说框架本身的复杂度被大大降低了可以把主要精力放在业务逻辑上这也是它适合做教学项目、毕设项目的核心原因。2. 开工前的设计思考数据表结构决定了工资计算的复杂度上限很多同学做这类系统上来就写类、写接口做到一半发现这个字段好像漏了这个逻辑表里没地方存被迫回去改表。这个项目我第一次做的时候也踩过类似的坑。后来重新理了一遍把数据模型设计看成整个系统的地基后面的开发就顺了很多。2.1 六张核心表员工、考勤、请假、工资、用户、部门这个系统最终落地的表结构是这样的我列出来供参考员工表employeeid、工号employee_no、姓名、性别、手机号、部门id、岗位id、入职时间、状态在职/离职、基本工资base_salary部门表departmentid、部门名称、部门编码、负责人id岗位表positionid、岗位名称、岗位编码、基础工资系数考勤表attendanceid、员工id、考勤日期、上班打卡时间、下班打卡时间、考勤状态正常/迟到/早退/缺卡、迟到分钟数、早退分钟数请假表leave_recordid、员工id、请假类型事假/病假/年假、开始时间、结束时间、请假天数、审批状态工资表salaryid、员工id、工资月份、应出勤天数、实际出勤天数、基本工资、岗位工资、绩效工资、加班工资、全勤奖、迟到扣款、请假扣款、应发工资、社保扣款、公积金扣款、个税、实发工资另外还有一张用户表sys_user和员工表做关联用来做登录认证。为什么要强调工资表里存的必须是计算结果而不是过程变量这里有个实际教训。第一次做的时候我在工资表里只存了应发工资和实发工资两个字段等到做统计报表的时候发现领导想看的是这个月全公司迟到的总分钟数各部门的请假天数对比结果数据全在考勤表里得临时聚合而且聚合逻辑分散在好几个service方法里改一个口径要动好几处。后来把工资表改成上面这种结果过程都冗余存储的结构需要任何维度的统计直接查工资表就行了效率高很多报表SQL也好写。2.2 工资和考勤怎么联动核心外键关系工资和考勤的联动是这个系统最核心的部分也是很多同类项目做得不清楚的地方。我的做法是工资表的员工id关联员工表主键再加上工资月份字段就构成了一条某个员工某个月份的工资记录的唯一定位。而考勤表通过员工id和考勤日期来记录每一天的打卡情况。到了每月工资计算时Service层先根据员工id和月份去考勤表里聚合出这个月的出勤天数、迟到天数、请假天数然后代入工资计算公式。这个设计最需要注意的地方是时间维度的处理。考勤是按天的工资是按月的如果你把考勤记录直接冗余到工资表里月底数据一变动工资表就得跟着更新非常痛苦。正确做法是考勤数据保持流水账结构工资计算时用统计查询来汇总这样既保持了考勤数据的原始性又不会出现数据不一致。这个思路其实就是数据仓库领域常说的事实表汇总表模式在这个小系统里虽然用不上那么高深的设计但思想是通用的。2.3 字段设计时容易忽略的两个细节金额字段不要用double用decimal。这是踩过坑之后才明白的。Java里的double和float在做浮点运算时有精度问题工资计算一涉及乘法和取整结果就是差个几分钱虽然不大但在财务场景里属于不可接受的错误。数据库里把金额字段定义为DECIMAL(10,2)Java侧用BigDecimal来对应计算都在这个类型上进行最后取两位小数彻底避免精度问题。状态字段用int而不用varchar同时用常量类做映射。比如员工状态0代表在职、1代表离职考勤状态0代表正常、1代表迟到、2代表早退、3代表缺卡。这样写的好处是数据库层面做统计和筛选非常方便索引效率高代码里定义好常量就不怕写错字符串。我第一次用varchar存正常迟到这种中文结果有的地方写正常有的地方写正常 带了个空格查出来的数据怎么都对不上。3. 核心功能模块的开发套路登录鉴权、考勤打卡与工资计算的代码组织表结构设计好了接下来就是具体代码怎么写。这一节我按功能模块来讲每个模块都给出实际可用的思路和关键代码片段而不是贴一堆无关紧要的完整代码。3.1 登录认证从Session到拦截器这个项目用的是一个比较简单但完全够用的登录方案Session 拦截器。用户登录后后端把用户信息存入Session同时写一个LoginInterceptor在请求进入Controller之前检查Session里有没有用户没有就跳回登录页。在Spring Boot里实现这个很简洁Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user request.getSession().getAttribute(loginUser); if (user null) { response.sendRedirect(/login); return false; } return true; } }然后注册进WebMvcConfigurerConfiguration public class WebConfig implements WebMvcConfigurer { Autowired private LoginInterceptor loginInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns(/**) .excludePathPatterns(/login, /user/login, /css/**, /js/**, /images/**); } }这样做了之后所有需要登录才能访问的页面和接口都统一保护起来新增功能模块时不用在每个Controller里重复写判断逻辑维护成本低了很多。值得展开说的是角色权限区分。系统里有两种角色管理员和普通员工。管理员能访问员工管理、工资管理、统计报表员工只能看自己的考勤和工资。这个用同一个Session里存的值来判断就可以但更优雅的做法是定义两个注解或者两个拦截器分别拦截不同的URL路径。比如/admin/**开头的地址只允许管理员访问普通员工访问就返回403页面。这个设计在项目答辩时比较容易说清楚也体现了一个基本的分层设计思路。3.2 考勤打卡日期边界和重复打卡的处理考勤是工资计算的数据源头所以打卡逻辑的严谨程度直接决定后面工资算得准不准。这个系统的打卡功能支持上下班打卡核心逻辑如下员工登录后点击上班打卡后端根据当前日期查询当天是否已有上班打卡记录。如果没有创建一条考勤记录记录上班打卡时间考勤状态暂存正常。如果有提示今天已打过上班卡不能重复打卡。下班打卡类似区别是需要先确认存在当天的上班打卡记录。判断迟到、早退的逻辑放在查询时动态计算而不是打卡时写死状态。这里有个细节容易出错考勤日期用当前日期还是对跨天情况做特殊处理。比如员工凌晨加班到第二天0点以后下班这算前一天的下班还是后一天的上班我的方案很简单——按自然日处理下班时间如果晚于当天23:59:59就记为当天23:59:59跨天加班通过加班申请补录。这样既避免了跨天的复杂逻辑又能满足绝大多数小型企业的实际需求。打卡时间和迟到判断的代码逻辑大概是这样的public MapString, Object punchClock(String employeeNo, String type) { LocalDate today LocalDate.now(); Attendance attendance attendanceMapper.selectByEmployeeAndDate(employeeNo, today); LocalTime now LocalTime.now(); if (work.equals(type)) { // 上班打卡 if (attendance ! null attendance.getWorkTime() ! null) { return error(今天已经打过上班卡); } if (attendance null) { attendance new Attendance(); attendance.setEmployeeId(employeeNo); attendance.setDate(today); } attendance.setWorkTime(now); // 假设9:00上班 LocalTime standardWork LocalTime.of(9, 0); if (now.isAfter(standardWork)) { long lateMinutes Duration.between(standardWork, now).toMinutes(); attendance.setLateMinutes((int) lateMinutes); } else { attendance.setLateMinutes(0); } attendanceMapper.insertOrUpdate(attendance); return success(打卡成功); } // 下班打卡同理判断是否早退 }这个方案里我特意把判断迟到和打卡动作放在一起处理其实有更好的做法打卡时只记录时间状态计算统一放在一个独立方法里考勤数据修正时直接重算。因为如果你在打卡时就把迟到的状态算好了一旦管理员发现某天网络有问题、时间不准手动补录考勤时又要重新跑一遍判断逻辑容易遗漏。3.3 工资计算把复杂的规则拆成一行一行的公式工资计算的逻辑是所有功能里最复杂的如果全写在一个方法里代码会非常臃肿且没法复用。我的做法是把工资计算拆成几个独立的方法每个方法负责一块逻辑Service public class SalaryCalculateService { public Salary calculateSalary(Long employeeId, String month) { Employee emp employeeMapper.selectById(employeeId); Salary salary new Salary(); salary.setEmployeeId(employeeId); salary.setMonth(month); // 1. 基础信息 BigDecimal baseSalary emp.getBaseSalary(); salary.setBaseSalary(baseSalary); // 2. 出勤统计 AttendanceStat stat attendanceMapper.statByMonth(employeeId, month); salary.setShouldDays(stat.getShouldDays()); salary.setActualDays(stat.getActualDays()); // 3. 各种扣款统计 BigDecimal lateDeduction calculateLateDeduction(stat); BigDecimal leaveDeduction calculateLeaveDeduction(employeeId, month); salary.setLateDeduction(lateDeduction); salary.setLeaveDeduction(leaveDeduction); // 4. 各种奖金 BigDecimal fullAttendanceBonus stat.getLateDays() 0 stat.getLeaveDays() 0 ? new BigDecimal(200) : BigDecimal.ZERO; salary.setFullAttendanceBonus(fullAttendanceBonus); // 5. 汇总 BigDecimal shouldPay baseSalary.add(fullAttendanceBonus) .subtract(lateDeduction) .subtract(leaveDeduction); salary.setShouldPay(shouldPay); // 6. 社保、公积金、个税 BigDecimal socialSecurity baseSalary.multiply(new BigDecimal(0.10)); BigDecimal housingFund baseSalary.multiply(new BigDecimal(0.07)); BigDecimal tax calculateTax(shouldPay.subtract(socialSecurity).subtract(housingFund)); salary.setSocialSecurity(socialSecurity); salary.setHousingFund(housingFund); salary.setTax(tax); salary.setActualPay(shouldPay.subtract(socialSecurity).subtract(housingFund).subtract(tax)); return salary; } }这里最重要的一个设计决策是每个方法只做一件事规则和规则之间靠方法的组合来串联。比如迟到扣款单独写一个方法请假扣款也单独写一个方法将来规则变了只需要改对应方法不影响其他部分。另外给个实际经验工资计算一定要设计成手动触发月度批量执行而不是每月1号自动就跑了。原因很简单实际场景中很多考勤数据可能需要几天才补录完全月初就算出来的工资是不准的。管理员的正确操作流程是先确认上月考勤数据全部完整再点击生成工资如果不小心算错了提供重新计算按钮可以将指定月份的工资数据全部删掉重新算。3.4 前端页面用Thymeleaf渲染还是前后端分离这个系统选的是服务端渲染方案Spring Boot内置的Thymeleaf模板引擎页面上直接写th:each、th:if这些属性来遍历数据、做条件判断。这个选择对毕设类项目来说是最省力的不需要额外搭建前端工程不需要处理跨域问题一个Controller方法直接返回视图名称就能完成页面跳转和数据填充。当然也有同学喜欢用Vue Element UI做前后端分离效果确实好看但配套要处理的东西也多接口文档、跨域配置、前端打包部署、登录状态的跨域共享。如果不是对前端很熟练建议不要在这个环节投入太多精力毕竟这类系统的核心评分点在后端业务逻辑和数据库设计上页面整洁、功能完整就足够了。4. 从拿到项目到跑起来环境配置的完整路径与运行视频的意义很多同学买毕设项目或者拿到学长给的源码后卡在第一步运行不起来然后就开始怀疑项目有问题。实际上绝大多数的运行失败不是因为代码错而是环境不一致。这个项目我拿到手的时候也折腾了半天最后总结出的运行步骤如下。4.1 环境版本匹配这是最容易被忽略的坑这个项目基于Spring Boot 2.x开发的对应的关键版本信息如下组件版本说明JDK1.8Spring Boot 2.x对JDK8支持最稳定Maven3.6用于依赖管理和项目打包MySQL5.7 / 8.05.7是最好的8.0需要注意连接驱动配置IDEIDEA 2020社区版也可以用不用必须专业版这里特别强调JDK版本。如果你电脑装的是JDK 17直接跑Spring Boot 2.x项目大概率会报各种反射、CGLIB相关的错误因为这些旧库对高版本JDK的模块化支持不友好。解决办法不是改代码而是给项目设置一个JDK 8的运行时环境。在IDEA里配置Project SDK和Module SDK都指定到JDK 8即可代码本身不需要改一行。同理MySQL的版本也有说法。如果是MySQL 8.0除了用mysql-connector-java的8.x版本之外还要在数据库连接URL后面加上useSSLfalseserverTimezoneAsia/Shanghai否则连接时不是报SSL警告就是时区错误。如果直接用5.7这些问题天然不存在。4.2 数据库初始化和配置连接项目里一般都会附带sql文件这个文件不光是给你导入数据库用的更是一份完整的表结构设计文档。我建议的导入流程是本地MySQL新建一个数据库名字可以和项目里配置文件中的库名保持一致。打开sql文件直接全选执行。注意看执行日志里有没有报错特别是外键约束相关的错误可能是导入顺序问题先导带外键的表会失败。打开项目的application.yml或者application.properties检查数据库连接信息spring: datasource: url: jdbc:mysql://localhost:3306/salary_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver这里最容易错的是密码。本地MySQL密码不是123456的话记得改配置文件还有就是如果密码里有特殊字符比如、#YAML文件里需要加引号包起来不然解析会出问题。4.3 运行视频到底有什么作用有人觉得运行视频可有可无其实对这类项目来说运行视频最大的价值在于帮助你预判运行过程中可能出现的画面和问题。你跟着视频操作一遍就是在用一个证明可行的路径来走通全流程。遇到报错时你至少能判断出是环境问题还是代码问题而不是对着一个报错信息一头雾水。我的建议是拿到项目后第一步先不要看源码先照着运行视频走一遍启动流程。哪怕你对项目里一行代码都不了解你先让项目跑起来心里就有了底后续看代码、改代码都有了对照点。这和学自行车一样先骑上去晃两圈再去看维修手册效率完全不同。5. 部署上线中最容易踩的五个坑及排查路径不管你是要本地演示还是放到服务器上跑下面这几个坑几乎是每个做Spring Boot项目的人都会遇到的。我把自己踩坑、排查、解决的过程写出来希望能帮你省去一遍遍百度的时间。5.1 端口被占用明明启动成功了却访问不到有一次启动项目时控制台报了Port 8080 was already in use提示端口被占用。当时的处理路径是用netstat -ano | findstr 8080查看谁占了端口。发现是另一个旧项目的进程没关掉。直接在任务管理器里结束对应进程或者修改当前项目的server.port配置。如果是Linux服务器用lsof -i:8080查看然后kill -9 进程号解决。这个问题虽然简单但在演示现场出了就很尴尬——提前启动确认账号密码能登录再开始讲别到答辩前五分钟手忙脚乱。5.2 白屏问题Thymeleaf模板解析失败后页面不渲染项目能启动但打开页面是空白或者报错TemplateInputException。这种基本都是模板引擎的问题检查pom.xml里有没有引入spring-boot-starter-thymeleaf依赖。检查模板文件后缀是不是.html位置应该在src/main/resources/templates/下。检查Controller返回的视图名称与模板文件名是否完全一致包括大小写。注意页面里如果写了th:object但表单里没有绑定对应字段也会解析报错。每次改完页面刷新不起作用的时候在IDEA里设置spring.thymeleaf.cachefalse开发阶段禁用缓存不然改了页面看不到效果。5.3 打包出来的jar包运行后找不到资源本地IDEA跑得好好的部署到服务器就是各种404、找不到静态资源。这个问题多发生在用mvn package打成Spring Boot通用jar包之后。Spring Boot把静态资源和页面模板都打进了jar内部如果你在服务器上手动解压或者用了外部路径配置就会出问题。正确姿势是直接用java -jar xxx.jar运行这个jar包Spring Boot的内置Tomcat会自动处理内部资源路径。如果你的项目确实依赖外部上传文件的目录比如头像图片需要在配置里指定file.upload-path这种外部存储路径然后通过映射配置提供访问。5.4 中文乱码从URL传递中文参数时全是问号这个问题出现在Tomcat的URL编码上。Spring Boot 2.x默认UTF-8编码基本不会乱但如果你用的服务器是外部Tomcat需要在server.xml里设置连接器的URIEncodingUTF-8。如果只是在IDEA里跑大概率是在Terminal的编码设置上把IDEA的全局编码改成UTF-8就行。数据库层面的乱码则是另一个问题那个是建表时字符集没设置好建表语句里带上DEFAULT CHARSETutf8mb4基本就能解决。5.5 数据库连接断开长时间页面空白然后报异常本地运行、期间不操作过一段时间再点页面报Communications link failure。这是因为MySQL默认的wait_timeout是8小时连接池里的连接如果超过这个时间空闲会被MySQL服务端断开而连接池并不感知这个变化拿到了已经失效的连接。解决办法有几种在JDBC连接URL上加autoReconnecttrue这只对5.x驱动有效对8.x已经不适用或者租用一个连接池比如HikariCP的配置之一设置max-lifetime和validation-timeout。最简单的方式是配置HikariCP的连接数验证spring: datasource: hikari: maximum-pool-size: 10 minimum-idle: 5 idle-timeout: 30000 max-lifetime: 1800000 connection-test-query: SELECT 1加上connection-test-query: SELECT 1这句是关键它会让连接池在获取连接之前先执行一个最简单的查询验证连接是否有效无效就重新建立连接。这样长时间空闲后再操作页面也不会有卡住的情况。6. 资料包里文档和讲解视频的正确用法一个完整的项目资料通常会包含源码、文档、运行视频、讲解视频四样东西。很多同学拿到的第一反应是打开源码一顿看其实资料的使用顺序很重要用对了能省不少时间。6.1 文档怎么看先看需求分析再看数据库设计项目文档一般包含开题报告、任务书、需求分析、系统设计、数据库设计、系统实现、测试、总结这些章节。我的建议是先看需求分析再看数据库设计最后看系统设计里的架构图代码部分完全不急着看。原因很简单需求分析告诉你这个系统该有什么功能数据库设计告诉你功能背后需要存哪些数据系统架构图告诉你这些功能是怎么被组织和调用的。有了这三层认知之后再打开代码去对照你就知道每个类、每个方法存在的必要性了。如果你反过来先看代码很容易陷入这个方法是干嘛的这个字段哪里来的这种细节里半天看不完一个模块。6.2 讲解视频怎么利用带着目的去听讲解视频一般是作者对项目做一个走读式的讲解从项目结构、代码组织到核心模块的实现思路全程过一遍。这类视频最适合在你已经把系统跑起来、并且看完数据库设计之后再看。建议以1.5倍速看完重点听以下几个部分作者讲某张表的时候对照自己手里的数据库设计文档看是否一致作者讲某个模块代码的时候暂停下来自己尝试独立写一遍不要只是看作者答疑的部分一般会提到一两个常见的坑直接记下来运行项目时留意一下。这样看完一遍视频整个项目等于已经过了三遍答辩时被问到任何细节都能接得上话。6.3 把别人的项目变成自己的项目的三步法最后说一个比较接地气的方法论。拿到开源或购买的毕设项目后直接一字不改拿去提交很容易被老师问穿帮因为代码风格、命名喜好、注释习惯都不像自己写的。我见过不少聪明的做法核心就是三步改造改界面上的文案。登录页的提示语、菜单名称、按钮文字全部换成自己的习惯表达不用大改但做完之后整个系统的性格就变了。改一个次要功能的实现方式。比如原来考勤导出用的EasyExcel你换成POI原来用的是MyBatis XML你改成MyBatis-Plus注解。不用动核心逻辑但答辩时被问到用没用过XX技术你可以理直气壮说用过了。自己重新画一遍数据库ER图。哪怕最终表结构没变你手绘理解过一遍老师问表关系就完全不虚。这一步虽然费时间但恰恰是最能证明你懂这个项目的地方。7. 这个系统做完了还能怎么扩展如果你做完这个毕业设计/课程设计之后还有富余时间我强烈建议你在功能完整性之外做以下三个方向的增强对学习深度和答辩效果都很有帮助。报表可视化增强。现有系统里工资和考勤都有数据了但页面多是表格展示。你可以集成一个ECharts库做几个统计页面月度工资总额对比柱状图、部门平均工资饼图、员工出勤率趋势折线图。技术上是前端图表库的引入再加上后端提供统计接口整个系统立刻显得高级不少。数据权限细分。目前的权限模型是管理员/员工两级可以细化成部门经理这个中间角色部门经理只能看本部门员工的数据。这个改动涉及数据库加字段、拦截器加逻辑、前端菜单动态渲染工作量适中但对设计能力的体现非常明显。员工自助操作。增加员工在系统中发起请假申请、查看自己的考勤日历、在线打印工资条这些功能。这会把系统的使用场景从管理员单机操作升级成全员协同使用更贴合真实企业应用。这三个方向不一定都做完挑一个顺手的方向做深就够了。面试或者答辩时一句我做的系统支持员工自助查询工资条比我能做CRUD有说服力得多。回到开头说的那个点工资考勤系统看似是个老题目但它覆盖的功能维度恰好横跨了基础数据管理、业务流程处理、规则汇总计算、权限控制、报表展示这几个后台系统必须的环节。静下心把每一环都做扎实收获的绝不仅仅是一个能交差的毕设而是一套以后做任何管理系统都能复用的思考方法和工程习惯。实际跑起来、改起来、用起来之后你自然会认同这个判断。
返回列表