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

资讯详情

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

基于SpringBoot+Vue3的员工考勤管理系统设计与实现

基于SpringBoot+Vue3的员工考勤管理系统设计与实现 先问一个很多做过考勤管理系统的人都会遇到的问题一个人力资源管理系统看起来很简单无非就是员工信息、请假审批、打卡记录、月度统计这几个菜单但为什么真正做完之后业务部门一用就会出现各种边界情况比如员工请了半天假应该算半天还是算一天员工早上十点才打卡是因为迟到还是因为下午班外勤人员没有固定班次打卡记录又该怎么归集如果把员工考勤管理系统做成“一张打卡表 一张请假表 几个统计页面”那确实不复杂。但一旦把班次、排班、请假、打卡、月度汇总、异常申诉这些都串起来系统复杂度会瞬间上来。本文以“基于 SpringBoot Vue3 前后端分离的员工考勤管理系统”为背景从业务模型、数据库设计、后端接口、Vue3 前端实现到联调验证拆解整个项目的实现过程。读完你会发现考勤系统真正的难点不在“管理”两个字而在“考勤规则如何落到工程实现上”。这一篇不会只给一张表结构图也不会只贴一段登录代码。我会把项目中容易返工的设计点、容易踩坑的时间边界、以及前后端分离部署时真正需要处理的问题都讲清楚方便你直接拿来做二次开发也方便你把技术方案迁移到其他管理类系统上。1. 考勤管理系统到底在管理什么很多从零开发考勤系统的开发者第一版往往会做这样的页面员工管理、部门管理、请假申请、请假审批、打卡记录、按月统计。看起来很完整但实际使用后会发现如下几个问题打卡记录是“流水”系统在页面里展示记录很容易但无法回答“这个月谁迟到了几次”。请假类型只有一个“请假天数”系统不知道“上午请假”和“全天请假”对工时计算的区别。员工打卡时间直接存成字符串统计时面对“今天应该几点上班”无从判断。管理员排班调整之后员工的历史考勤结果没有重新计算机制。所以考勤管理系统并不是“管理员工考勤记录”而是“根据排班规则、请假规则、打卡原始数据计算出员工每一天的考勤结果”。这是整个系统的核心判断。开发顺序应该是先定义班次例如 09:00-18:00午休 12:00-13:00迟到阈值 10 分钟。再为员工排班张三 2025-04-01 上早班。再保存员工打卡原始数据。最后按班次计算张三当天打卡 08:55是否迟到是否缺卡实际工时多少判断的核心逻辑不属于 Vue3 页面也不属于 SpringBoot 的 Controller而是一套独立的考勤计算规则。页面只是负责展示结果和录入原始数据。如果你把“迟到”“早退”这种计算结论直接存进打卡表一开始会觉得很方便但后期规则一变历史数据就全部作废。合理的设计是“只存原始事实规则用于计算”。2. 技术选型SpringBoot Vue3 的分工逻辑这里先明确一个观点前后端分离不只是流行趋势而是这种管理项目天然的团队协作与部署方式。后端只负责输出 JSON 数据前端只负责渲染页面和交互。对于员工考勤管理系统来说用 SpringBoot Vue3 的组合是合理选择。2.1 SpringBoot 的职责后端核心职责包括提供员工、部门、班次、排班、请假、打卡、统计等模块的 RESTful API。处理权限认证例如登录后发放 Token受保护接口校验 Token。校验数据合法性例如请假开始时间不能大于结束时间。执行考勤统计、月度汇总等计算任务。对接 MySQL 数据库。后端不关心用户看到的是什么按钮只负责把数据可靠地读写出来。2.2 Vue3 的职责前端核心职责包括登录页面保存 Token处理登录态。员工、班次、排班等管理页面负责表单填写和列表展示。考勤打卡页面员工可以点击“上班打卡/下班打卡”。请假申请页面员工提交申请审批人查看列表并审批。数据看板页面用图表展示当月出勤率、迟到次数、请假天数等。Vue3 比较适合这种中后台管理系统配合 Element Plus 组件库页面开发效率很高。Vue3 的 Composition API 让逻辑复用比 Vue2 的 options API 更清晰比如封装一个“当前登录用户信息”的逻辑在不同页面直接调用即可。2.3 为什么建议用前后端分离而不是服务端模板渲染如果你的最终项目要部署到服务器用 SpringBoot 直接返回页面也可以Thymeleaf 或 JSP 并没有过时。但实际开发中考勤管理系统往往需要多端展示比如 PC 后台、移动端 H5、甚至后续接入钉钉/企业微信前端视图层和后端服务层如果耦合在一起后续扩展会非常痛苦。前后端分离后改动前端样式不影响后端接口。例如员工打卡页面要增加定位信息只需要在 Vue3 页面中调用浏览器 Geolocation API再把经纬度传给后端后端接口完全可以复用。如果使用服务端模板渲染这种逻辑会散落到服务端代码里不好维护。所以这个项目的推荐分工是SpringBoot 负责业务规则与数据持久化Vue3 负责展示、表单校验、交互状态。二者的桥梁是 JSON。3. 从需求到模块先拆分再开发开发这种管理系统最容易犯的错误是一上来就写代码。正确的做法是先把需求拆成模块每个模块之间只通过接口交互。下面以实际可落地的模块划分方式来说明。3.1 核心模块清单模块功能说明典型页面系统管理用户登录、权限控制、密码修改登录页组织管理部门维护、员工档案维护员工列表、部门树班次管理定义上下班时间、午休时间、迟到阈值班次列表排班管理为员工分配某一天的班次排班表考勤打卡员工上下班打卡记录来源、时间、位置打卡页请假管理员工提交请假单审批人处理请假申请、审批考勤统计查看某员工/部门某月的出勤汇总考勤月报、仪表盘模块划分好之后后端包结构基本可以参考前端菜单来建立避免 controller、service、mapper 全部堆在一个包里。3.2 前端菜单和后端接口的对应关系先设计菜单能反过来约束数据库表字段避免页面需要的数据查不到。比如“考勤月报”页面需要展示员工姓名、部门、应出勤天数、实际出勤天数、迟到次数、请假天数那么后端至少需要能在这些维度上进行查询。如果后期想展示更多比如加班时长那么数据库中排班表和打卡记录表的设计就要支持跨天处理。这里的经验是每一个页面都至少要有一个明确的“数据来源”和“接口契约”。4. 数据库设计考勤系统的核心表数据库设计就是这个项目最大的分水岭。如果表设计不对后面统计逻辑会非常痛苦。这里给出一个可以直接落地的核心表设计思路不会把所有字段写全但会说明每个表的职责边界。4.1 员工表与部门表员工表除了基本档案还应该有“状态”字段例如在职、离职。做年度考勤统计时离职员工不应该出现在当月的应出勤人选中。部门表结构相对简单主要提供层级关系。如果权限要求部门管理员只能查看本部门数据部门表中要有 parent_id 这类字段。4.2 班次表班次表定义员工一天的上下班规则字段建议如下CREATE TABLE attendance_shift ( id BIGINT PRIMARY KEY AUTO_INCREMENT, shift_name VARCHAR(50) NOT NULL COMMENT 班次名称, on_duty_time TIME NOT NULL COMMENT 上班时间, off_duty_time TIME NOT NULL COMMENT 下班时间, late_minutes INT DEFAULT 0 COMMENT 迟到多少分钟算迟到, early_minutes INT DEFAULT 0 COMMENT 早退多少分钟算早退, is_cross_day TINYINT DEFAULT 0 COMMENT 是否跨天例如晚班22:00-次日06:00 );这里要说明一个关键点如果一个班次是“22:00 到次日 6:00”直接把 off_duty_time 存成 06:00在排班判断时会出现日期归属问题。因为“4月1日 22:00 上班”对应“4月2日 6:00 下班”统计 4 月 2 日的结果时如果不做跨天处理本次上班就会被漏掉或者算错。最稳妥的做法是在班次表中增加 is_cross_day 字段。计算时如果跨天班次上班日期是 4 月 1 日下班打卡时间即使出现在 4 月 2 日凌晨也归入 4 月 1 日这个班次对应的工作日。很多“看起来正常”的考勤系统其实都没处理好跨天开发和测试时需要重点覆盖。4.3 排班表排班表把员工和班次、日期关联起来CREATE TABLE attendance_schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, employee_id BIGINT NOT NULL COMMENT 员工ID, work_date DATE NOT NULL COMMENT 上班日期, shift_id BIGINT NOT NULL COMMENT 班次ID, UNIQUE KEY uk_employee_date (employee_id, work_date) );这一张表决定了“谁在哪一天应该按照什么班次上班”。如果公司没有排班流程而是固定朝九晚五也可以通过初始化脚本把每一天的排班生成出来。但只要有轮班制度就必须使用排班表。员工是否迟到不应该拿打卡记录和“默认早上九点”做比较应该和排班表指定的班次上班时间做比较。4.4 打卡原始记录表打卡记录表只保存原始事实不做计算。字段可以分为两类一类是识别员工另一类是记录时间与来源。CREATE TABLE attendance_punch ( id BIGINT PRIMARY KEY AUTO_INCREMENT, employee_id BIGINT NOT NULL COMMENT 员工ID, schedule_id BIGINT NULL COMMENT 排班ID可空用于无排班打卡, punch_time DATETIME NOT NULL COMMENT 打卡时间由服务端生成避免前端时间不准, punch_source VARCHAR(20) DEFAULT PC COMMENT 打卡来源PC/H5/车载等, longitude DECIMAL(10,6) NULL, latitude DECIMAL(10,6) NULL, punch_type TINYINT DEFAULT 0 COMMENT 0未知 1上班 2下班 3外勤, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_employee_punch (employee_id, punch_time) ) COMMENT打卡原始记录表;这里有意识地设计了 schedule_id 字段但考虑到员工可能在无排班的情况下被要求临时到岗打卡schedule_id 允许为空。打卡类型区分“上班”“下班”时系统优先按当天排班自动判断而不是让人工选择因为人工选择很容易出现重复打“上班卡”这类问题。4.5 请假申请表请假申请不只是记录“请假两天”更重要的字段是开始时间、结束时间和时长类型。CREATE TABLE leave_apply ( id BIGINT PRIMARY KEY AUTO_INCREMENT, employee_id BIGINT NOT NULL, leave_type VARCHAR(20) NOT NULL COMMENT 年假/事假/病假, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, leave_days DECIMAL(4,1) DEFAULT 0, reason VARCHAR(500), status VARCHAR(20) DEFAULT PENDING COMMENT PENDING/APPROVED/REJECTED/CANCELLED, approver_id BIGINT, approve_time DATETIME, reject_reason VARCHAR(500) );请假审批通过之后它在考勤月报中应该抵消掉对应的应出勤时长。请半天事假、一天年假这些情况会在月度统计 SQL 中与排班和打卡记录联合计算。5. SpringBoot 后端核心实现完成表设计后再进入后端接口开发就会顺畅很多。下面从工程结构、打卡逻辑、统计逻辑、请假审批四个方面来说。5.1 后端工程目录建议一个常规的 SpringBoot 多模块内部结构可以这样规划注意这里的目录层次要贴合你的 Maven 或 Gradle 工程src/main/java/com/attendance ├── AttendanceApplication.java ├── common // 通用返回体、异常处理、常量 ├── config // 安全配置、拦截器配置、跨域配置 ├── controller // 接口层 ├── service // 业务层 ├── mapper // 数据访问层 ├── entity // 数据库实体 ├── dto // 入参对象 └── vo // 出参对象Controller 只负责接收参数、校验基础格式、调用 service 并返回结果不要把 SQL 或计算逻辑写在 Controller 里。Service 层处理核心规则Service 层之间按业务上下文划分例如打卡、请假审批、统计可以拆成不同 Service。5.2 打卡接口核心逻辑打卡是考勤系统最核心的操作。这里给出一个简化版判断逻辑如果员工当天没有排班则允许记录但标记为“无排班打卡”如果有排班第一次有效打卡自动匹配为上班卡或异常卡后续匹配为下班卡。这里不做过度编码而是把思路和关键代码写出来。// 文件路径src/main/java/com/attendance/service/impl/PunchServiceImpl.java Override public PunchResultVO punch(PunchDTO dto, Long employeeId) { LocalDate today LocalDate.now(); LocalTime now LocalTime.now(); // 1. 查询当天排班 Schedule schedule scheduleMapper.findByEmployeeAndDate(employeeId, today); // 2. 查询当天已有打卡记录 ListAttendancePunch records punchMapper.findByEmployeeAndDate(employeeId, today); AttendancePunch punch new AttendancePunch(); punch.setEmployeeId(employeeId); punch.setPunchTime(LocalDateTime.now()); punch.setLatitude(dto.getLatitude()); punch.setLongitude(dto.getLongitude()); if (schedule null) { punch.setScheduleId(null); punch.setPunchType(PunchType.UNKNOWN.getCode()); punchMapper.insert(punch); return buildResult(punch, 当前无排班已记录为异常打卡); } Shift shift shiftMapper.findById(schedule.getShiftId()); punch.setScheduleId(schedule.getId()); if (records.isEmpty()) { // 没有上班卡则判定这次打卡是否为上班卡 boolean onDuty isBetween(now, shift.getOnDutyTime().minusMinutes(10), shift.getOnDutyTime().plusMinutes(90)); punch.setPunchType(onDuty ? PunchType.ON_DUTY.getCode() : PunchType.UNKNOWN.getCode()); punchMapper.insert(punch); return buildResult(punch, onDuty ? 上班打卡成功 : 时间与班次不符已记录为异常打卡); } boolean hasOnDuty records.stream() .anyMatch(r - PunchType.ON_DUTY.getCode() r.getPunchType()); if (hasOnDuty isBetween(now, shift.getOffDutyTime().minusMinutes(90), shift.getOffDutyTime().plusMinutes(30))) { punch.setPunchType(PunchType.OFF_DUTY.getCode()); punchMapper.insert(punch); return buildResult(punch, 下班打卡成功); } punch.setPunchType(PunchType.UNKNOWN.getCode()); punchMapper.insert(punch); return buildResult(punch, 当前时间不在上下班窗口内已记录为异常打卡); }实际项目中这个逻辑还需要考虑如果上晚班时间窗口应该基于排班日期偏移计算不能直接拿 LocalTime 比较。如果员工补卡通常需要审批流程不是直接调用这个接口。打卡时间应该以服务器时间为准不能信任前端传上来的时间。这里要特别注意一点虽然我们在 Punch 表中保留了 PunchType但这些类型只是“识别结果”并不代表最终考勤状态。最终是迟到还是早退应该由统计模块基于班次时间再次计算。如果打卡类型判断错也可以通过后台修正而不影响历史统计口径。5.3 月度考勤统计实现思路月度统计不能用 Java 代码把一个月的数据全部拉出来再循环判断那样性能差且难以维护。更好的做法是用 SQL 先聚合再加上 Java 做局部复杂判断。统计需求通常是给定开始日期、结束日期、员工ID查询每一天的应出勤班次、实际上班时间、实际下班时间并在返回结果中组装迟到、早退、缺勤状态。一个可行的查询思路如下-- 查询某员工某时间段的出勤明细 SELECT s.work_date, sh.shift_name, sh.on_duty_time, sh.off_duty_time, MAX(CASE WHEN p.punch_type 1 THEN DATE_FORMAT(p.punch_time, %H:%i:%s) END) AS on_duty_actual, MAX(CASE WHEN p.punch_type 2 THEN DATE_FORMAT(p.punch_time, %H:%i:%s) END) AS off_duty_actual FROM attendance_schedule s LEFT JOIN attendance_shift sh ON s.shift_id sh.id LEFT JOIN attendance_punch p ON p.schedule_id s.id AND p.employee_id s.employee_id WHERE s.employee_id #{employeeId} AND s.work_date BETWEEN #{beginDate} AND #{endDate} GROUP BY s.work_date, sh.shift_name, sh.on_duty_time, sh.off_duty_time ORDER BY s.work_date;之所以使用 LEFT JOIN是为了把“没有打卡记录”的排班也查出来如果没有记录on_duty_actual 就是 NULLJava 层识别为缺卡即可。在 Java 中把查询结果转换为 VO再比对规则即可if (item.getOnDutyActual() null) { item.setStatus(未打卡); } else if (item.getOnDutyActual().isAfter(onDutyLateLine)) { item.setStatus(迟到); }需要注意这里是“迟到的红线时间”不是上班时间。比如班次上班时间是 09:00迟到阈值是 10 分钟那么红线是 09:10。SQL 中判断会麻烦在 Java 内存中判断会更直观。5.4 请假审批的状态机请假模块不建议直接用一个字符串字段存状态然后前端随便修改。更稳妥的做法是定义状态流转提交后为 PENDING审批通过为 APPROVED拒绝为 REJECTED申请人撤销为 CANCELLED。后端在修改状态时要校验当前状态是否允许目标状态流转。例如 APPROVED 状态不能直接由员工改成 CANCELLED需要走撤销或反审核流程。请假审批通过后考勤月报需要自动扣除当天的应出勤。处理方式可以是在统计查询时把已审批的请假时间段排除也可以用一个 daily_attendance_summary 表提前把月度结果算好请假通过后再重新生成对应日期汇总。个人更推荐在查询阶段实时计算避免维护汇总表与业务表的一致性。6. Vue3 前端页面实现要点前端部分不必把所有 CSS 都贴出来重点是几个能够在实际开发中直接复用的结构axios 封装、路由守卫、打卡页交互逻辑。6.1 前端工程目录建议一个中后台管理项目如果用 Vite 构建目录通常包含如下部分src ├── api // 按模块封装的请求 ├── assets ├── components // 通用组件 ├── router // 路由 ├── stores // Pinia 状态管理 ├── views // 页面 ├── utils // axios 等工具 └── App.vue使用 axios 封装需要解决两个问题请求时自动携带 Token响应时统一处理业务异常和登录过期。// 文件路径src/utils/request.js import axios from axios import { ElMessage } from element-plus import router from /router const service axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || /api, timeout: 15000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(error.message || 网络异常) return Promise.reject(error) } ) export default service这里的返回结构依赖后端统一返回体设计。建议后端所有接口都返回类似{ code: 200, message: success, data: ... }的结构方便前端统一处理避免每个页面单独判断。6.2 路由守卫登录页面和业务页面的路由需要区分。业务页面应该验证是否存在 Token否则跳转登录页。// 文件路径src/router/index.js import { createRouter, createWebHistory } from vue-router const router createRouter({ history: createWebHistory(), routes: [ { path: /login, component: () import(/views/Login.vue) }, { path: /dashboard, component: () import(/views/Dashboard.vue), meta: { requiresAuth: true } } ] }) router.beforeEach((to) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { return /login } if (to.path /login token) { return /dashboard } return true }) export default router实际项目中还可以通过菜单数组动态生成路由但第一次开发不建议过度设计先把静态路由跑通再考虑权限按钮级别的控制。6.3 考勤打卡页面的核心代码员工打卡页面展示当前时间和今日打卡状态。页面点击“上班打卡/下班打卡”时调用后端接口。这里有一个常见误区不要在页面里计算“应该打上班卡还是下班卡”这个判断应该交给后端。!-- 文件路径src/views/Dashboard.vue -- script setup import { ref, onMounted } from vue import request from /utils/request import { ElMessage } from element-plus const todayInfo ref({}) const currentTime ref() let timer null function loadToday() { request.get(/attendance/today).then(data { todayInfo.value data }) } async function handlePunch() { try { const data await request.post(/attendance/punch) ElMessage.success(data.message || 打卡成功) loadToday() } catch (e) { // 错误提示已在拦截器中统一处理 } } onMounted(() { loadToday() timer setInterval(() { currentTime.value new Date().toLocaleString() }, 1000) }) onUnmounted(() { clearInterval(timer) }) /script页面上的定位信息如果要接入可以把 navigator.geolocation.getCurrentPosition 获取到的经纬度附加到请求体中。不过浏览器定位需要用户授权而且在 HTTP 环境下部分浏览器限制比较严格生产环境尽量使用 HTTPS。Vue3 中另一个常见需求是使用 ECharts 做考勤看板。如果在 Vue3 中使用 ECharts建议封装一个按需引入的图表组件避免在 main.js 全量引入造成打包体积过大。图表数据来自后端统计接口渲染时只需要关注 option 的更新而不是手动操作 DOM。7. 前后端联调与跨域处理使用 Vite 开发时最方便的联调方式是配置代理。把/api开头的请求转发到 SpringBoot 服务端口同时 SpringBoot 侧也允许来自前端开发服务器的跨域请求。7.1 Vite 代理配置// 文件路径vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })如果后端接口路径已经是/api/xxx代理里就不需要 rewrite。这里要保证前端 axios 的 baseURL 与代理前缀一致例如baseURL: /api。后面把打包后的静态文件放到 Nginx再把/api反向代理到后端地址开发环境与生产环境的请求路径可以保持一致。7.2 SpringBoot 跨域配置即使开发环境使用了 Vite 代理浏览器请求的地址依然是http://localhost:5173并没有真正跨域。但如果你在测试环境直接访问后端 Swagger 或调试接口还是需要后端支持跨域。SpringBoot 中可以在 WebMvcConfigurer 中统一配置。// 文件路径src/main/java/com/attendance/config/CorsConfig.java Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }生产环境不建议allowedOriginPatterns(*)这样全放开更稳妥的是只配置前端域名。正文里放开是为了开发调试方便生产环境务必收紧。7.3 后端统一返回体设计前后端分离项目中统一返回结构非常重要。后端所有接口如果成功返回 data失败返回 code 和 message前端 axios 拦截器处理逻辑会非常干净。这里给出一种最基础的结构{ code: 200, message: success, data: { workDate: 2025-04-01, onDutyTime: 08:55:00 } }同时要避免后端直接抛出异常而是通过 RestControllerAdvice 捕获并转换为统一返回结构。这样前端就不会因为“后端 500 错误”而弹出非预期提示。8. 运行与效果验证系统开发完成后不能只验证“能登录、能跳转”还要验证最核心的考勤规则。建议准备一套测试用例。8.1 后端启动后端启动前检查数据库连接、Redis如果引入了等配置。启动成功后访问类似/swagger-ui.html或/doc.html可以查看接口文档。如果使用了 Sa-Token 或 JWT 拦截器注意将登录接口和 Swagger 文档接口加入白名单否则会直接 401导致无法调试 Interface。mvn spring-boot:run如果项目使用的是 SpringBoot 3.xJDK 版本需要对应调整Maven 依赖也要使用适配 SpringBoot 3 的版本。如果主工程是从其他地方复制来的优先检查 spring-boot-starter-parent 的版本避免依赖冲突。8.2 前端启动npm install npm run dev如果启动时出现vite命令无法识别可能是没有安装依赖或 Node.js 版本过低。Vite 需要 Node.js 环境Vue3 项目建议使用 LTS 版本 Node.js遇到版本过高导致无法启动的情况时降低 Node.js 版本通常比改代码更省时间。8.3 核心自测用例建议在页面和接口层面验证以下几个场景场景操作预期结果正常上班打卡上班时间前 5 分钟打卡标记为上班打卡状态为正常迟到打卡上班时间后 30 分钟打卡统计结果中状态为迟到无排班打卡当天没有排班的员工打卡记录保存但标记为异常打卡请假跨天提交 4 月 1 日 12:00 到 4 月 2 日 18:00 请假审批通过后统计应扣除对应工时跨天班次22:00 到次日 06:00 班次次日凌晨打卡归入前一天的排班登录过期携带过期 Token 请求业务接口返回 401 并跳转登录页面如果自测时发现跨天班次的打卡没有归入正确日期首先要检查日期计算使用的时钟是否统一。推荐在服务端使用系统默认时区时显式设置 JDBC 连接参数避免数据库服务与时区问题导致 DATETIME 差异。9. 考勤系统常见问题排查除了跨天问题实际开发中经常遇到的还有下面这些情况。问题现象可能原因排查方式解决方案前端请求接口报 404Vite 代理路径与后端 context-path 不一致浏览器 Network 查看实际请求地址统一代理前缀或删除后端 context-path登录后刷新页面又跳回登录页Token 只保存在内存变量里没有存 localStorage查看请求头中是否有 Authorization登录后写入 localStorage 或 Pinia 持久化插件打卡时间与本地时间相差 8 小时JDBC 连接没有配置 serverTimezone查看数据库连接的 URL在 JDBC URL 中增加 serverTimezoneAsia/Shanghai同一员工同一分钟内重复打卡页面重复点击后端没有做幂等控制查看数据库是否有重复记录数据库增加唯一索引 后端判断上一次打卡时间部门管理页面树形结构错乱parent_id 指向错误检查部门表数据增加校验禁止删除有子部门的部门SpringBoot 启动时 Bean 冲突多个实现类没有加限定名查看启动日志中 NoUniqueBeanDefinitionException使用 Qualifier 或 Primary 指定实现前端打包后刷新页面 404后端没有做前端 history 路由回退Nginx 配置中检查 try_filesNginx 配置try_files $uri $uri/ /index.html;其中打卡幂等和时区问题最容易被忽视。很多团队开发第一阶段功能正常上线后才被员工反馈“我明明只打了一次卡后台怎么出现两条记录”。数据库的唯一索引只能挡住同一秒内完全一致的记录但无法判断员工是“故意补卡”还是“误触”。所以在业务层还需要加一个规则两次打卡时间间隔小于某个阈值时不重复写入例如 10 秒内不重复保存。时区问题更隐蔽。如果本地开发时数据库和服务器都在同一台机器上往往测不出来。一旦部署到云服务器数据库使用 UTC 时间服务端使用北京时间SQL 中DATE_FORMAT或 Java 中LocalDate.now()就可能出现错位。统一方案是数据库连接 URL 和服务端 JVM 时区都明确设置为 Asia/Shanghai数据库中的 datetime 只存业务时间不存当前时间戳。10. 工程最佳实践与安全建议从“能跑”到“能在生产环境稳定运行”中间还差很多工程化细节。10.1 密码与权限员工的登录密码不能明文存储。Spring Security 中的 BCryptPasswordEncoder 可以完成密码加密也可以使用常见的加盐哈希方案。没有引入 Security 的情况下可以使用 Hutool 工具类或 Shiro但不要自己造加密算法。接口权限建议避免“后端只是登录时校验其他接口不鉴权”的问题。一个最简单的角色权限方案是登录后把用户角色放入 Token后端通过拦截器判断当前用户是否能访问某类接口。这里需要强调一个原则前端隐藏菜单只是体验设计真正的权限必须由后端每个接口校验。10.2 敏感数据接口员工手机号、身份证号、家庭住址等属性不应该在员工列表接口中完整返回。普通列表只需要返回员工姓名、部门、职位等用于展示的内容。只有导出工资类报表或极少数管理员操作时才提供带敏感数据的接口。这类接口即便面向内网也应该加上操作日志和二次校验。10.3 数据库变更与备份考勤数据一旦生成就存在后续审计或追溯需求。所以在开发阶段就要养成一个习惯凡是修改排班规则、请假类型或审批状态都记录操作人和操作时间。对于生产环境任何批量修改 SQL 都要先备份原表比如执行 update 前先导出受影响记录的 select 结果避免误操作后无法回滚。10.4 统一日志对考勤系统来说打卡记录、请假审批这些属于高频操作日志不能只打印到控制台。建议给 SpringBoot 配置 Logback 日志文件按天滚动保存。在打卡接口中打印员工ID、打卡时间、来自哪个 IP 或设备。这样后面出现“员工说打过卡但系统没记录”的纠纷时至少可以通过日志核对调用情况。10.5 前后端分离部署生产环境有两种主流部署方式。第一种是后端 SpringBoot 打包成 JAR 单独运行前端 Vue3 构建后放到 Nginx 或 CDNNginx 将/api请求反代到后端服务。第二种是全部打包成 Docker 镜像后端一个容器、前端一个容器再用 Nginx 容器做反向代理。不管哪种方式前端都尽量不要把静态资源丢进 SpringBoot 的 static 目录否则就失去了前后端分离的独立扩展意义。10.6 权限模型不要一开始就做太复杂管理类系统绕不开权限但第一次实现时不建议直接把 IDEA 里的 RBAC 设计成多对多的完整权限框架那样会在用户、角色、菜单、按钮权限上写大量关联表反而拖慢开发节奏。先做两到三层超级管理员、HR/考勤专员、普通员工。普通员工只能看到自己的考勤和请假申请考勤专员可以看到本部门或全部员工的排班和统计。等核心业务跑通再逐步增加更细的菜单权限和按钮权限才是比较健康的演进顺序。11. 这类项目的后续扩展方向做完第一版员工考勤管理系统后面可以继续扩展的方向其实很多。第一个方向是接入移动端 H5 或小程序让员工在手机上进行上下班打卡和请假申请。现在 Vue3 项目可以直接复用大部分代码如果之后用 uni-app 或其他跨端框架后端接口不需要调整这正好体现前后端分离设计的好处。第二个方向是增加排班周历视图。很多公司不是固定班次而是每周围定轮转页面需要在日历上显示每天的班次类型。前端使用日历组件时后端只需要把某员工某月的排班数据一次性返回日历组件根据日期渲染即可。第三个方向是引入规则引擎。当考勤规则变得复杂时例如节假日自动替换班次、加班审批、调休抵扣等把考勤算法规整到独立 Engine 类中并配置固定的规则枚举不要散落在多个 Service 中。如果未来要支持不同公司的差异化规则才考虑引入 Drools 或 Flowable 这类规则引擎。对于大多数员工考勤系统来说一个逻辑清晰的 Java 类比引入重型规则引擎更可控。最后提醒一点项目的命名编号是否规范不重要重要的是开发的每一步都保证代码和数据结构可以被后续版本平滑扩展。项目里的员工管理、部门管理、班次排班、考勤打卡、请假审批、月度汇总每一块都是通用业务能力的沉淀。如果你从头到尾独立完成一遍再回头去看市面上的任意一套后台管理框架理解和迁移速度都会快很多。建议把文章中提到的表结构、接口分层规则和自测用例作为清单自己动手从空项目开始搭建并跑通一次完整流程再用 Vue3 补齐页面会比只收藏这篇文章有用得多。
返回列表