ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

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员工IDclock_datedate打卡日期clock_in_timedatetime上班打卡时间clock_out_timedatetime下班打卡时间statustinyint状态1正常2迟到3早退4异常这张表可以说是整个系统的核心。建议把上下班打卡拆成两个字段而不是两条记录这样在统计每日考勤状态时效率更高。如果你需要支持一天多次打卡比如午休前后各打一次可以再加字段或者拆子表但基础架构不宜一开始就搞太复杂。请假表leave_request字段名类型说明idbigint主键employee_idbigint员工IDleave_typetinyint假别1事假2病假3年假4调休start_timedatetime开始时间end_timedatetime结束时间reasonvarchar(255)请假事由statustinyint审核状态0待审1通过2驳回考勤统计表attendance_summary字段名类型说明idbigint主键employee_idbigint员工IDsummary_monthvarchar(7)统计月份如2025-06work_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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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 groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency然后在application.yml里配置数据源和 MyBatis-Plus 相关参数server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/attendance?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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 接口只需要继承BaseMapperMapper public interface EmployeeMapper extends BaseMapperEmployee { }Service 层的核心逻辑也很简单主要是对LambdaQueryWrapper的使用比如按姓名或工号搜索public ListEmployee searchEmployees(String keyword) { LambdaQueryWrapperEmployee 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 LambdaQueryWrapperAttendanceRecord() .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. 获取所有在职员工 ListEmployee employees employeeMapper.selectList( new LambdaQueryWrapperEmployee().eq(Employee::getStatus, 1) ); for (Employee emp : employees) { // 2. 计算应出勤天数这里可以结合国家法定节假日或者用工作日计算工具类 int workDays calculateWorkDays(month, emp); // 3. 查询该员工当月打卡记录 ListAttendanceRecord records attendanceMapper.selectList( new LambdaQueryWrapperAttendanceRecord() .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目录下就行。具体流程是前端项目执行npm run build会生成dist目录。把dist目录下的所有文件复制到 Spring Boot 的src/main/resources/static/目录下。重新打包 Spring Boot 项目执行mvn clean package。启动 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.timezoneGMT8或者在application.yml中配spring: jackson: time-zone: GMT8定时任务重复执行也很常见尤其在集群部署时。课程设计一般不需要考虑分布式锁但如果是在真实项目中建议引入 Quartz 集群模式或者用 Redis 分布式锁避免多个实例同时跑统计。6.5 前端部署后刷新页面 404 的处理如果你坚持用 Vue Router 的 history 模式后果就是部署后访问非首页路径时刷新会 404。这个问题的本质是浏览器向 Spring Boot 请求了一个不存在的后端路由。解决方案是在后端加一个简单的控制器把非/api开头的所有请求转发到index.htmlController public class PageForwardController { RequestMapping(value /{path:[^\\.]*}) public String forward() { return forward:/index.html; } }但要注意这个控制器必须保证/**/*.js、*.css等静态资源优先被处理否则静态文件也会被转发。所以放行规则要写严谨或者干脆用 hash 模式一了百了。7. 项目演示与后续扩展建议系统开发完成之后建议准备一份演示脚本按“登录→员工管理→打卡→请假审批→考勤统计”这条主流程走一遍。演示过程中可以刻意展示几个亮点比如打卡后状态自动更新为迟到的效果请假审批后统计报表随之变化以及月度统计的定时任务可以手动触发并展示数据变化。这些环节能直观说明系统不是写死的假页面而是真正有业务逻辑在跑的。关于后续扩展如果你还有余力或想要更高的完成度我建议优先做这几件事引入 Redis 缓存把部门列表、员工基础信息这类高频读取、低频更新的数据放进缓存减轻数据库压力。增加考勤异常申诉流程员工可以对被误判的迟到早退发起申诉管理员审核后修正状态。这个功能在真实业务里几乎是标配写在简历上非常亮眼。对接企业微信或钉钉的打卡数据这个方向适合做毕设的同学能体现你在接口对接方面的能力不过难度会略微提升。导出考勤报表到 Excel用 Easy Excel 就可以实现几十行代码搞定却能让实用性提升一大截。我个人在实际开发这类系统后最大的感受是考勤系统技术上不算难但它非常考验业务梳理能力。你能不能在动手前把规则想清楚、把边界场景列出来直接决定了后面代码的稳定程度。如果你也正在做类似的 Spring Boot 项目不用怕踩坑遇到问题先确认是不是数据库设计或版本兼容导致的再一步步排查。代码写丑了可以重构数据库设计乱了才是真正的灾难。希望这篇内容能帮你把思路理顺少走几个弯路。
返回列表