news 2026/10/9 0:05:37

Spring Boot考勤系统全栈开发实战:从数据库设计到部署避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot考勤系统全栈开发实战:从数据库设计到部署避坑

1. 项目概述:从零搭建一套能用的考勤系统,到底难在哪

先聊点实在的。提起“员工考勤系统”,很多人第一反应是“这不就是个打卡记录吗,有什么好做的”。但真正接过这类需求的人都知道,考勤系统最麻烦的从来不是打卡本身,而是背后那一堆业务规则:迟到怎么算、早退怎么判、请假调休怎么抵扣、加班时长怎么和排班挂钩……稍微多几个班次、多几种假别,逻辑就会迅速膨胀。而如果恰好在用 Spring Boot 做这套东西,还得同时考虑员工管理、部门层级、权限区分、前端接口设计、数据报表展示这些配套模块,工作量和复杂度一下子就上来了。

这篇内容主要面向两类人:一类是正在做课程设计或毕业设计的学生,需要一套能跑通、结构清晰、能写进论文里的 Spring Boot 考勤系统;另一类是刚入行的开发,想看看别人是怎么把考勤这类业务需求拆解成技术方案的。我会按照实际开发顺序,完整梳理从需求分析到数据库设计、从后端接口到前端页面、从部署到避坑的全部过程,把我个人踩过的坑和总结出的经验一并放出来,希望能帮你少走弯路。

这个项目我用的是 Spring Boot 2.7 + MyBatis-Plus + MySQL 8.0 + Vue 2 + Element UI 这套组合。选型理由后文会详细说,这里先给结论:这是一套最主流、最不缺参考资料、也最容易在答辩或汇报时讲清楚的技术栈,对新手极其友好。

2. 需求设计与技术选型:先想清楚再做,比写代码重要得多

2.1 考勤系统的核心需求拆解

很多人拿到题目直接建表写接口,结果写到一半发现业务逻辑对不上,数据库改了七八遍。我建议先花半天时间把需求列表写出来,哪怕只是在纸上列个清单,后面都能省下大把返工时间。

员工考勤系统最基础的需求,无非这么几条:

  • 员工信息管理:包括工号、姓名、所属部门、职位、入职时间、状态等基本档案。
  • 考勤打卡记录:支持上班打卡、下班打卡,能记录每次打卡的具体时间。
  • 考勤规则配置:包括上下班时间、迟到早退的时间阈值、工作日设置、是否启用弹性打卡等。
  • 请假与出差管理:员工提交申请,管理员审核,并自动关联到考勤统计中。
  • 考勤统计与报表:按日、按月统计出勤天数、迟到次数、早退次数、缺勤记录,可以导出查看。
  • 角色权限:普通员工只能看自己的考勤,部门主管能看本部门,管理员能看全公司。

如果再细化一些,比如考勤异常申诉、加班时长计算、排班管理、调休管理,这些都是加分项,可以看时间安排来选择做还是不做。

我的经验是:第一次做这类系统,优先保证基础流程闭环,也就是“员工打卡→生成考勤记录→管理员审核→统计报表”这个主线,先把这条链路走通,再加复杂规则。否则很容易陷入“规则越加越多、代码越改越乱”的泥潭。

2.2 为什么选 Spring Boot + MyBatis-Plus + Vue

先说说后端框架。Spring Boot 生态成熟、自动化配置省心,适合快速搭建业务系统。而且考勤系统这个题目在面试和答辩中很常见,面试官基本默认你懂 Spring Boot,所以它作为主框架是最稳妥的选择。

持久层我没用传统 MyBatis 的 XML 写一堆手写 SQL,而是选了 MyBatis-Plus。MyBatis-Plus 提供了通用的单表 CRUD 接口,像员工表、部门表这种简单增删改查基本不用写 SQL,可以大大缩短开发周期。遇到多表关联查询时再写自定义 SQL,灵活性也足够。如果我没记错,MyBatis-Plus 的 BaseMapper 里已经封装了 insert、deleteById、selectById、selectPage 这些常用方法,直接用就行。

前端选了 Vue 2 + Element UI。坦白讲,Vue 3 是趋势,但如果你是为了快速完成课程设计,Vue 2 的参考资料多,Element UI 组件库文档全,网上现成的后台管理模板也几乎都是基于这套组合。与其花大量时间折腾组合兼容性问题,不如把精力放在业务功能上。当然,如果你已经熟悉 Vue 3 + Element Plus,直接用也没问题,接口层面完全不受影响。

2.3 项目整体架构设计

我采用的前后端分离架构,后端提供 RESTful API,前端通过 Axios 请求接口。项目结构如下:

attendance-system ├── backend │ ├── src/main/java/com/example/attendance │ │ ├── controller # 接口层 │ │ ├── service # 业务逻辑层 │ │ ├── mapper # MyBatis-Plus 数据访问层 │ │ ├── entity # 实体类 │ │ ├── dto # 数据传输对象 │ │ ├── config # 配置类(拦截器、跨域、异常处理) │ │ └── utils # 工具类 │ ├── src/main/resources │ │ ├── mapper # 自定义 SQL XML(需要时用) │ │ └── application.yml # 配置文件 ├── frontend │ ├── src │ │ ├── api # 接口封装 │ │ ├── views # 页面组件 │ │ ├── router # 路由 │ │ ├── store # 状态管理 │ │ └── utils # 工具类 └── sql └── attendance.sql # 数据库初始化脚本

这样分层的核心思想是:Controller 只负责参数接收和结果返回,不写业务逻辑;Service 层专注处理考勤规则、统计计算这些核心逻辑;Mapper 层只跟数据库打交道。层与层之间通过接口调用,互相不渗透。这一点在答辩时非常加分,因为考官一眼就能看出你具备基本的工程化思维。

3. 数据库设计:考勤系统的地基,直接决定后续开发的顺畅程度

3.1 表结构设计思路

数据库设计的好坏,决定了你后面写代码时要多写多少冗余逻辑。我的核心思路是:主表尽量精简,关联表按业务拆分。强烈不建议把所有字段堆在一张表里,比如把打卡记录和请假记录混在一张表,统计时会让你生不如死。

我的数据库至少包含以下五张核心表:

员工表(employee)

字段名类型说明
idbigint主键,自增或雪花算法生成
emp_novarchar(20)工号,唯一索引
namevarchar(50)姓名
department_idbigint部门ID,关联部门表
positionvarchar(50)职位
phonevarchar(20)联系电话
hire_datedate入职日期
statustinyint状态:1在职,0离职

部门表(department)

字段名类型说明
idbigint主键
dept_namevarchar(50)部门名称
parent_idbigint上级部门ID,用于树形结构

打卡记录表(attendance_record)

字段名类型说明
idbigint主键
employee_idbigint员工ID
clock_datedate打卡日期
clock_in_timedatetime上班打卡时间
clock_out_timedatetime下班打卡时间
statustinyint状态:1正常,2迟到,3早退,4异常

这张表可以说是整个系统的核心。建议把上下班打卡拆成两个字段而不是两条记录,这样在统计每日考勤状态时效率更高。如果你需要支持一天多次打卡(比如午休前后各打一次),可以再加字段或者拆子表,但基础架构不宜一开始就搞太复杂。

请假表(leave_request)

字段名类型说明
idbigint主键
employee_idbigint员工ID
leave_typetinyint假别:1事假,2病假,3年假,4调休
start_timedatetime开始时间
end_timedatetime结束时间
reasonvarchar(255)请假事由
statustinyint审核状态:0待审,1通过,2驳回

考勤统计表(attendance_summary)

字段名类型说明
idbigint主键
employee_idbigint员工ID
summary_monthvarchar(7)统计月份,如2025-06
work_daysint应出勤天数
actual_daysint实际出勤天数
late_countint迟到次数
early_countint早退次数
absent_daysint缺勤天数
leave_daysint请假天数

统计表最好做物化处理,也就是通过定时任务或手动触发计算后落库,而不是每次查询时实时聚合。原因很简单:当数据量过万之后,实时聚合的速度会越来越慢,考官或领导看到页面转圈超过三秒就会皱眉。定时汇总方案更符合真实业务场景。

3.2 为什么要把迟到早退状态在打卡时就算出来

这里分享一个我在开发过程中踩过的坑。最初我设计的打卡记录表里只存储打卡时间,考勤状态是在统计数据时才去判断的,结果就是各种查询条件组合在一起,SQL 越写越长,而且数据量大了后性能直线下降。后来我把状态判断逻辑前移,在员工打卡时就直接算好当天的考勤状态并写入记录表,统计报表时只需要做简单的 count 聚合,轻松很多。

同时要注意,考勤状态可能会被后续的请假申请影响。比如某位员工早上打了卡,但上午突发急事请了病假,那么理论上他当天的出勤状态可能需要修正。我的处理办法是:请假审批通过后,将请假时间范围内的打卡记录状态自动修正为“请假”,并在统计表中扣除相应的出勤天数。这属于业务规则的典型坑,提前设计好能避免后期手忙脚乱。

3.3 数据库脚本示例

下面给出员工表和打卡记录表的简化建表 SQL,供你参考:

CREATE TABLE `employee` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `emp_no` varchar(20) NOT NULL COMMENT '工号', `name` varchar(50) NOT NULL COMMENT '姓名', `department_id` bigint(20) DEFAULT NULL COMMENT '部门ID', `position` varchar(50) DEFAULT NULL COMMENT '职位', `phone` varchar(20) DEFAULT NULL COMMENT '联系电话', `hire_date` date DEFAULT NULL COMMENT '入职日期', `status` tinyint(1) DEFAULT '1' COMMENT '1在职 0离职', PRIMARY KEY (`id`), UNIQUE KEY `uk_emp_no` (`emp_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工表'; CREATE TABLE `attendance_record` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `employee_id` bigint(20) NOT NULL COMMENT '员工ID', `clock_date` date NOT NULL COMMENT '打卡日期', `clock_in_time` datetime DEFAULT NULL COMMENT '上班打卡时间', `clock_out_time` datetime DEFAULT NULL COMMENT '下班打卡时间', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1正常 2迟到 3早退 4异常', PRIMARY KEY (`id`), KEY `idx_employee_date` (`employee_id`, `clock_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='打卡记录表';

这里有个关键细节:attendance_record表上一定要建立联合索引(employee_id, clock_date),因为绝大多数查询都是“查某个员工某段时间的记录”,没有这个索引的话,数据量稍微上来一点,查询就会明显变慢。这是我曾经用慢查询日志抓出来的教训,真的不能省。

4. 后端实现:Spring Boot 接口开发与考勤规则落地的核心代码

4.1 项目初始化与配置

创建 Spring Boot 项目时,我建议直接用 Spring Initializr,选好 Java 8 或 11(不要选太高版本,否则一些老依赖可能不兼容),勾选 Spring Web、MySQL Driver、MyBatis-Plus 依赖。MyBatis-Plus 需要单独添加依赖坐标,因为它不在 Spring Initializr 的列表里。

<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency>

然后在application.yml里配置数据源和 MyBatis-Plus 相关参数:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/attendance?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: id-type: auto

其中map-underscore-to-camel-case一定要设为 true,这样数据库的clock_in_time字段就能自动映射到 Java 的clockInTime属性,省去一堆手写映射的麻烦。

4.2 员工管理模块的快速实现

员工模块属于典型的单表 CRUD,用 MyBatis-Plus 非常快。先建实体类:

@Data @TableName("employee") public class Employee { @TableId(type = IdType.AUTO) private Long id; private String empNo; private String name; private Long departmentId; private String position; private String phone; private LocalDate hireDate; private Integer status; }

然后写 Mapper 接口,只需要继承BaseMapper:

@Mapper public interface EmployeeMapper extends BaseMapper<Employee> { }

Service 层的核心逻辑也很简单,主要是对LambdaQueryWrapper的使用,比如按姓名或工号搜索:

public List<Employee> searchEmployees(String keyword) { LambdaQueryWrapper<Employee> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(keyword), Employee::getName, keyword) .or() .like(StringUtils.hasText(keyword), Employee::getEmpNo, keyword); return employeeMapper.selectList(wrapper); }

这一段逻辑虽然简单,但有两个经验值得说。一是LambdaQueryWrapper比QueryWrapper更安全,因为它是通过方法引用来获取字段名,编译时就能发现字段写错的问题。二是条件构造器里的hasText判断是必备的,否则前端传个空字符串过来,like会自动变成全表扫描的%%,把整个表的数据都查出来了,数据量大时很尴尬。

4.3 打卡接口的设计与实现

打卡是整个系统的核心接口,设计时要注意两点:一是要防止同一员工同一分钟重复打卡;二是要能准确判断迟到早退。

先定义考勤规则配置类:

@Data public class AttendanceRule { private LocalTime workStartTime; // 上班时间,比如 09:00 private LocalTime workEndTime; // 下班时间,比如 18:00 private Integer lateThreshold; // 迟到阈值(分钟),超过即算迟到 private Integer earlyThreshold; // 早退阈值(分钟) }

打卡接口的核心逻辑:

public AttendanceRecord clockIn(Long employeeId, LocalDateTime clockTime) { // 防止重复打卡:当天已经打过上班卡则报错 LocalDate today = clockTime.toLocalDate(); AttendanceRecord record = attendanceMapper.selectOne( new LambdaQueryWrapper<AttendanceRecord>() .eq(AttendanceRecord::getEmployeeId, employeeId) .eq(AttendanceRecord::getClockDate, today) ); if (record != null && record.getClockInTime() != null) { throw new BusinessException("今天已经打过上班卡了"); } if (record == null) { record = new AttendanceRecord(); record.setEmployeeId(employeeId); record.setClockDate(today); record.setClockInTime(clockTime); // 判断是否迟到 if (clockTime.toLocalTime().isAfter(rule.getWorkStartTime().plusMinutes(rule.getLateThreshold()))) { record.setStatus(2); // 迟到 } else { record.setStatus(1); // 正常 } attendanceMapper.insert(record); } else { record.setClockOutTime(clockTime); // 判断是否早退 if (clockTime.toLocalTime().isBefore(rule.getWorkEndTime().minusMinutes(rule.getEarlyThreshold()))) { record.setStatus(3); // 早退 } attendanceMapper.updateById(record); } return record; }

这段代码要注意一个细节:如果员工上午迟到后,下午正常下班,那最终状态应该保留“迟到”,下班打卡时不能把状态覆盖成正常。所以我只在clockOutTime为 null 时更新状态,否则保留原状态。类似的边界情况在实际开发中非常容易碰到,建议提前思考清楚。

4.4 考勤统计的逻辑实现

统计模块是我认为整个项目中最有技术含量的一部分。你需要处理应出勤天数、实际出勤天数、迟到次数、早退次数、缺勤天数、请假天数等多项指标。

我的实现思路是这样的:在每个月第一天(或月底),由定时任务触发生成上个月的统计汇总。统计的核心逻辑是:

public void generateMonthlySummary(String month) { // 1. 获取所有在职员工 List<Employee> employees = employeeMapper.selectList( new LambdaQueryWrapper<Employee>().eq(Employee::getStatus, 1) ); for (Employee emp : employees) { // 2. 计算应出勤天数(这里可以结合国家法定节假日,或者用工作日计算工具类) int workDays = calculateWorkDays(month, emp); // 3. 查询该员工当月打卡记录 List<AttendanceRecord> records = attendanceMapper.selectList( new LambdaQueryWrapper<AttendanceRecord>() .eq(AttendanceRecord::getEmployeeId, emp.getId()) .likeRight(AttendanceRecord::getClockDate, month) ); // 4. 统计各项指标 long lateCount = records.stream().filter(r -> r.getStatus() == 2).count(); long earlyCount = records.stream().filter(r -> r.getStatus() == 3).count(); long absentDays = workDays - records.size(); // 5. 写入汇总表 AttendanceSummary summary = new AttendanceSummary(); summary.setEmployeeId(emp.getId()); summary.setSummaryMonth(month); summary.setWorkDays(workDays); summary.setActualDays(records.size()); summary.setLateCount((int) lateCount); summary.setEarlyCount((int) earlyCount); summary.setAbsentDays((int) absentDays); attendanceSummaryMapper.insert(summary); } }

这里面的calculateWorkDays是一个比较麻烦的方法。如果你不想引入节假日组件,可以先简单按“周一至周五为工作日”来计算,然后在需求说明或论文里注明“未考虑法定节假日”,这样在答辩时也不会被追问得太深。如果想做得更严谨,可以引入holiday-calendar之类的库,或者维护一张法定节假日表,这也是一个很好的加分项。

4.5 每月统计定时任务的实现方式

Spring Boot 提供了非常方便的@Scheduled注解来实现定时任务。在启动类或配置类上加上@EnableScheduling,然后在统计方法上加上@Scheduled即可。

@Component public class AttendanceSummaryTask { @Autowired private AttendanceSummaryService summaryService; // 每月1日凌晨2点执行上个月的统计 @Scheduled(cron = "0 0 2 1 * ?") public void generateLastMonthSummary() { LocalDate lastMonth = LocalDate.now().minusMonths(1); String month = lastMonth.format(DateTimeFormatter.ofPattern("yyyy-MM")); summaryService.generateMonthlySummary(month); } }

cron 表达式是秒 分 时 日 月 周,所以0 0 2 1 * ?表示每月1日凌晨2点执行。这里要注意:cron表达式里的*和?不能混用错位,周字段如果不想指定,必须用?而不是*,这点已经被好多同学踩过坑了。定时任务触发的统计建议放在凌晨执行,避开员工打卡高峰,也避免大数据量统计影响数据库性能。

5. 前端页面与接口对接:Vue 打包部署到 Spring Boot 的那些事

5.1 Vue 项目核心页面设计

前端我采用 Vue 2 + Element UI 搭建。页面大方向就三类:登录与权限,员工管理页面,考勤记录页面。

员工管理页面用el-table展示员工列表,配合el-dialog实现新增和编辑功能,再用el-pagination做分页。这些是 Element UI 的基本用法,网上模板一抓一大把,我不重复讲了。这里重点说两个容易出问题的细节。

第一个是日期格式化问题。Element UI 的el-date-picker返回的默认格式是Date对象,如果你直接把它传给后端,JSON 序列化后可能是“2025-06-01T16:00:00.000Z”这种带时区格式,后端 LocalDateTime 直接解析会报错。我统一在 Axios 请求拦截器里对参数做处理,把日期统一格式化为yyyy-MM-dd HH:mm:ss字符串再提交:

axios.interceptors.request.use(config => { if (config.data instanceof FormData) { return config; } // 递归处理 Date 类型转字符串 config.data = JSON.parse(JSON.stringify(config.data, (key, value) => { if (value instanceof Date) { return dayjs(value).format('YYYY-MM-DD HH:mm:ss'); } return value; })); return config; });

第二个是跨域问题。前后端分离开发时,前端跑在 9527 端口(Vue 默认),后端跑在 8080,浏览器会拦截跨域请求。最简单的本地解决办法是配置 Vue 的开发服务器代理,在vue.config.js里加上:

module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '' } } } } };

这样前端请求/api/employee/list时,实际上会被代理到http://localhost:8080/employee/list,完美避开跨域问题。当然线上部署时后端也要配置跨域或者使用 Nginx 反向代理,但开发阶段用 proxy 是最省事的。

5.2 把 Vue 打包后放进 Spring Boot 的方法与注意事项

这个需求在热词里出现了很多次,应该是很多同学在部署环节会卡住的问题。Spring Boot 默认是把前端当静态资源来处理的,不需要单独部署 Nginx,直接通过 Maven 把打包后的前端文件放进src/main/resources/static目录下就行。

具体流程是:

  1. 前端项目执行npm run build,会生成dist目录。
  2. 把dist目录下的所有文件复制到 Spring Boot 的src/main/resources/static/目录下。
  3. 重新打包 Spring Boot 项目,执行mvn clean package。
  4. 启动 jar 包,浏览器访问http://localhost:8080/index.html就能看到前端页面。

这里有两个细节千万注意。第一,Vue 2 默认的路由模式是 hash 模式,也就是 URL 上会带#,这种方式在打包部署时最省心,不需要额外配置。如果你用了 history 模式,刷新页面时后端会返回 404,必须在后端写一个 fallback 控制器,把未知路由指回index.html,操作比较繁琐。我的建议是能用 hash 就别用 history。

第二,如果前端和后端通过同一个域名端口访问,前端请求接口时建议用相对路径,比如axios.get('/api/employee/list')。如果把接口地址写死成http://localhost:8080/api/...,打包部署后就要改代码重新构建,非常麻烦。在前后端分离部署到同一 jar 的情况下,相对路径是唯一舒服的解法。

5.3 登录权限与拦截器实现

考勤系统不登录就说不过去。我使用的是 JWT 方案,实现方式很经典:用户登录成功后后端生成一个 token 返回给前端,前端把 token 存在本地,之后每次请求在 Header 里带上Authorization: Bearer <token>,后端通过拦截器校验 token。

Spring Boot 拦截器实现如下:

public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if ("OPTIONS".equals(request.getMethod())) { return true; // 放行预检请求 } String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); try { // 解析 token,校验签名和过期时间 Claims claims = JwtUtil.parseToken(token); request.setAttribute("userId", claims.get("userId")); return true; } catch (Exception e) { response.setStatus(401); return false; } } response.setStatus(401); return false; } }

最容易被忽略的是OPTIONS请求放行。当你在前端通过 Axios 携带自定义 Header 跨域请求时,浏览器会先发送一个 OPTIONS 预检请求,这个请求是没有 token 的,如果不放行,前端会一直报跨域错误。我当时排查这个问题花了好几个小时,所以这里特别提醒你。

同时,拦截器注册时要注意排除登录接口和静态资源路径,否则正常页面都进不去。在WebMvcConfigurer里这么写:

@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/login", "/api/register", "/**/*.html", "/**/*.js", "/**/*.css", "/**/*.png", "/**/*.ico"); }

6. 常见问题与排查技巧实录:这些坑我替你踩过了

6.1 Spring Boot 版本太高导致的依赖兼容问题

很多同学一上来就选最新版 Spring Boot,结果遇到各种诡异问题。比如 Spring Boot 3.x 必须搭配 Java 17 和 Jakarta EE,导致一堆旧教程里的javax.*包全部失效,网上资料又少,稍不留神就被卡住几天。

我的建议是:课程设计或中小型项目,直接用 Spring Boot 2.7.x 搭配 Java 8。这是经过大量生产环境验证的版本组合,资料最多、踩坑答案最全、面试官也熟悉。不要盲目追求新版本,项目的核心是完成业务,而不是追赶框架升级。

6.2 MyBatis-Plus 字段映射失败与 LocalDateTime 序列化问题

map-underscore-to-camel-case配置了但字段映射还是失败?最常见的毛病是实体类没有加@TableName注解,或者表名前缀不一致。另外LocalDateTime在 Jackson 序列化时默认格式是一长串数组,前端拿到根本没法显示,需要在配置中指定格式:

@Bean public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { return builder -> builder.simpleDateFormat("yyyy-MM-dd HH:mm:ss") .serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))) .deserializers(new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); }

6.3 打卡记录查询慢?索引和分页一起上

当考勤记录表超过十万条时,不带条件的全表扫描会明显变慢。解决办法是确保组合索引生效,同时采用 MyBatis-Plus 的分页插件。分页插件配置很简单:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

分页插件不只是简单加 limit,它会在查询前先执行 count 查询,统计总条数。如果你觉得 count 查询性能有瓶颈,可以手动优化 count SQL,但一般业务场景够用了。另外一定记得查询条件里始终带上employee_id和clock_date区间,这样才能命中联合索引。

6.4 定时任务重复执行或时区错乱

如果部署到服务器后定时任务在“凌晨2点”执行,但实际日志时间差了8小时,大概率是服务器时区没设置成 Asia/Shanghai。在启动命令或配置里加-Duser.timezone=GMT+8,或者在application.yml中配:

spring: jackson: time-zone: GMT+8

定时任务重复执行也很常见,尤其在集群部署时。课程设计一般不需要考虑分布式锁,但如果是在真实项目中,建议引入 Quartz 集群模式或者用 Redis 分布式锁,避免多个实例同时跑统计。

6.5 前端部署后刷新页面 404 的处理

如果你坚持用 Vue Router 的 history 模式,后果就是部署后访问非首页路径时刷新会 404。这个问题的本质是浏览器向 Spring Boot 请求了一个不存在的后端路由。解决方案是在后端加一个简单的控制器,把非/api开头的所有请求转发到index.html:

@Controller public class PageForwardController { @RequestMapping(value = "/{path:[^\\.]*}") public String forward() { return "forward:/index.html"; } }

但要注意,这个控制器必须保证/**/*.js、*.css等静态资源优先被处理,否则静态文件也会被转发。所以放行规则要写严谨,或者干脆用 hash 模式,一了百了。

7. 项目演示与后续扩展建议

系统开发完成之后,建议准备一份演示脚本,按“登录→员工管理→打卡→请假审批→考勤统计”这条主流程走一遍。演示过程中可以刻意展示几个亮点:比如打卡后状态自动更新为迟到的效果,请假审批后统计报表随之变化,以及月度统计的定时任务可以手动触发并展示数据变化。这些环节能直观说明系统不是写死的假页面,而是真正有业务逻辑在跑的。

关于后续扩展,如果你还有余力或想要更高的完成度,我建议优先做这几件事:

  • 引入 Redis 缓存,把部门列表、员工基础信息这类高频读取、低频更新的数据放进缓存,减轻数据库压力。
  • 增加考勤异常申诉流程,员工可以对被误判的迟到早退发起申诉,管理员审核后修正状态。这个功能在真实业务里几乎是标配,写在简历上非常亮眼。
  • 对接企业微信或钉钉的打卡数据,这个方向适合做毕设的同学,能体现你在接口对接方面的能力,不过难度会略微提升。
  • 导出考勤报表到 Excel,用 Easy Excel 就可以实现,几十行代码搞定,却能让实用性提升一大截。

我个人在实际开发这类系统后最大的感受是:考勤系统技术上不算难,但它非常考验业务梳理能力。你能不能在动手前把规则想清楚、把边界场景列出来,直接决定了后面代码的稳定程度。如果你也正在做类似的 Spring Boot 项目,不用怕踩坑,遇到问题先确认是不是数据库设计或版本兼容导致的,再一步步排查。代码写丑了可以重构,数据库设计乱了才是真正的灾难。希望这篇内容能帮你把思路理顺,少走几个弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 0:05:08

Flutter鸿蒙化适配:如何用分层结构重构analysis_options配置

做 Flutter 鸿蒙化适配的团队&#xff0c;基本都会撞上同一个尴尬场景&#xff1a;把三方库拉到鸿蒙 SDK 工程里&#xff0c;跑一遍flutter analyze&#xff0c;屏幕上几千条 warning 和 info 刷下来&#xff0c;一半是“平台差异”造成的误报&#xff0c;另一半却是真问题。本…

作者头像 李华
网站建设 2026/10/7 21:47:00

在线协作 Presence 实战:从光标同步到协作体温

在线协作工具的体验&#xff0c;拆到最后往往只剩下两个词&#xff1a;快&#xff0c;和&#xff0c;在场。快解决的是效率问题&#xff0c;在场解决的是信任问题。Presence 插件在我们项目里承担的就是后者——让每个人能看见"谁在旁边、正在做什么、光标停在哪一行"…

作者头像 李华
网站建设 2026/10/7 21:45:22

Java微信小程序学习打卡系统:从源码跑通到项目实战

简介&#xff1a;这份资源是面向Java与微信小程序方向的学生及开发者的一套完整项目实践包&#xff0c;以日常学习打卡系统为主题&#xff0c;适合用作毕业设计、课程设计或前后端协同开发的练手案例。项目采用Java后端配合微信小程序前端&#xff0c;并引入云开发能力&#xf…

作者头像 李华
网站建设 2026/10/7 21:44:23

AI语音伪造检测实战:基于MFCC和TensorFlow的深度学习实现

简介&#xff1a;这套基于深度学习的AI语音伪造检测实战项目&#xff0c;面向语音安全与模式识别方向的中级开发者&#xff0c;针对性解决文本转语音及GAN合成语音难以辨识的问题&#xff0c;可应用于金融交易、身份核验等高风险场景。项目以Python为语言基础&#xff0c;集成T…

作者头像 李华
网站建设 2026/10/7 21:44:06

MCP服务器运维实战:工具选型、配置与排错指南

先说个很实际的场景&#xff1a;你手上管着十几台服务器&#xff0c;白天刚处理完一台Windows 2016的端口策略问题&#xff0c;晚上又收到告警说某台Linux机器磁盘快满了。你熟练地打开SSH客户端、敲df -h、netstat、翻日志、查防火墙规则&#xff0c;这一套流程每天重复无数次…

作者头像 李华
网站建设 2026/10/7 21:43:23

AI编程智能体复现指南:RepoMaster与GitTaskBench实战

1. 项目概述&#xff1a;RepoMaster 和 GitTaskBench 到底在解决什么问题先说结论&#xff1a;RepoMaster 是一个面向真实仓库级别任务的代码智能体框架&#xff0c;GitTaskBench 则是一个用来衡量这类智能体到底行不行的基准测试集合。两个项目放在一起复现&#xff0c;核心目…

作者头像 李华