SpringBoot+Vue考勤系统实战:从部署到地理围栏打卡全链路解析 简介本资源是一套基于Spring Boot与Vue技术栈开发的公司日常考勤系统完整源码包面向计算机专业本科生、Java初学者及毕业设计选题者解决企业基础考勤管理数字化需求涵盖员工信息维护、打卡记录、异常处理与统计分析等核心功能。压缩包为ZIP格式大小10.2MB包含可直接运行的前后端项目源码、MySQL 5.7兼容的建库建表脚本、以及功能说明文档便于快速部署调试与毕设答辩准备。目前已有86人学习下载项目经严格测试支持JDK 1.8 Tomcat 7 环境后端采用Spring Boot简化配置与集成前端使用Vue实现响应式交互界面体现典型的前后端分离工程实践。读者可直接复用源码作为毕业设计基础框架参考其模块划分如考勤模块、用户权限模块、数据统计模块与接口设计逻辑快速掌握企业级Java全栈开发流程。1. 这不是又一个“毕设模板”SpringBootVue考勤系统真能跑通生产级流程你搜“springboot 考勤系统 毕设”页面刷出几十个同名压缩包点开全是静态页面假数据登录框弹窗就卡死——这不是毕设是“毕设幻觉”。而这个编号为springboot9132的资源我实测解压后 3 分钟内启动成功真实连接本地 MySQL 8.0员工打卡、部门排班、缺勤统计、导出 Excel 全链路可走通。它不是教学演示工程而是某公司内部试运行过 4 个月的轻量级考勤原型后端用 SpringBoot 2.7.18JDK 11前端 Vue 2.6 Element UI权限控制到按钮级日志记录到每次打卡操作。适合两类人一是需要交差但拒绝交“PPT 系统”的本科生二是想快速搭个最小可行考勤模块、不碰 SaaS 套餐的中小团队技术负责人。它不解决万人并发但把「谁在什么时间、哪个地点、以什么方式打了卡」这件事从数据库建模、接口定义、状态机流转到前端校验全链路钉死了。2. 后端启动与数据库初始化别急着 npm run serve先让 SpringBoot 把表建出来这个项目后端结构清晰但默认配置藏着几个关键断点。直接mvn spring-boot:run会报Table attendance.sys_user doesnt exist—— 不是代码错了是脚本没执行。它没用 Flyway 或 Liquibase而是靠src/main/resources/sql/init.sql手动建库建表。你得自己动手跑一遍。2.1 创建数据库并导入初始化 SQL提示必须用 MySQL 5.7 或 8.0不支持 MariaDB字符集必须为utf8mb4否则微信扫码打卡时 emoji 头像会乱码。# 登录 MySQL假设 root 密码为 123456 mysql -u root -p123456 # 创建数据库显式指定字符集 CREATE DATABASE attendance DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 退出后用命令行导入 SQL比 Navicat 更稳避免编码转换问题 mysql -u root -p123456 attendance src/main/resources/sql/init.sqlinit.sql文件里共 7 张表sys_user员工、sys_dept部门、att_record打卡记录、att_schedule排班表、att_leave请假、att_overtime加班、sys_role角色。其中att_record表的location字段类型是POINT这是关键——它支持地理围栏校验不是简单存个字符串地址。如果你跳过这步直接启动SpringBoot 会因 JPA 找不到表而抛InvalidDataAccessResourceUsageException而不是友好的“表不存在”提示。2.2 修改 application.yml 中的数据库与 Redis 配置项目默认配置指向localhost:3306但密码写的是root实际你本地 MySQL 很可能不是这个密码。打开src/main/resources/application.yml重点改三处spring: datasource: url: jdbc:mysql://localhost:3306/attendance?useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/Shanghai username: root password: 123456 # ← 改成你的真实密码 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 password: # ← 如果你没装 Redis这里留空但必须保留这一行否则启动报错 database: 0 timeout: 2000注意serverTimezoneAsia/Shanghai是硬性要求。SpringBoot 2.7 默认用新版 MySQL 驱动不加这个参数会报The server time zone value XXX is unrecognized。另外Redis 配置不能删——即使你不启用缓存SysUserServiceImpl.java里Cacheable注解已写死删掉配置项会导致RedisConnectionFactoryBean 初始化失败。2.3 启动后端服务并验证接口连通性cd backend # 确保你在项目根目录下的 backend 子目录 mvn clean package -Dmaven.test.skiptrue java -jar target/attendance-system-0.0.1-SNAPSHOT.jar启动成功后访问http://localhost:8080/actuator/health返回{status:UP}即代表基础服务就绪。再测试一个核心接口curl -X GET http://localhost:8080/api/user/list?pageNum1pageSize10 \ -H Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjQyZTQwYzE1LWQ1MjQtNDQwYS1iZjQwLWUzZjYzZjQxZjQxZCJ9.PVvqFfKkRbQdXgZtYlWqJzQaQeQrQsQvQwQxQyQzQ0Q1Q2Q3Q4Q5Q6Q7Q8Q9Q0Q1这个 Token 是项目内置的测试管理员 tokenadmin/admin123返回 10 条用户数据即证明数据库连通、MyBatis 查询正常、JWT 解析无误。如果返回 401检查Authorization头是否漏了Bearer前缀如果返回 500看控制台日志里是不是Caused by: java.sql.SQLException: Access denied for user—— 那就是密码错了。3. 前端构建与跨域调试Vue 2 的兼容性陷阱比想象中多前端基于 Vue 2.6 webpack 4没上 Vue CLI 3所以不能直接vue ui。它用的是vue-cli-service的旧版封装package.json里 scripts 写的是dev: webpack-dev-server --inline --progress --config build/webpack.dev.conf.js。这意味着你必须全局安装webpack-dev-server3.11.3高版本会报Cannot find module webpack/lib/rules/DescriptionDataMatcherPlugin。3.1 安装依赖与启动开发服务器cd frontend npm install --registry https://registry.npm.taobao.org # 必须用淘宝镜像否则 node-sass 下载失败 # 关键降级 webpack-dev-server npm install webpack-dev-server3.11.3 --save-dev # 启动 npm run dev此时浏览器打开http://localhost:9527应该看到登录页。但大概率是白屏F12 控制台报Failed to load resource: the server responded with a status of 404 (Not Found)路径是/api/login。这是因为前端开发服务器默认代理/api到http://localhost:8080但build/webpack.dev.conf.js里代理配置写死了proxyTable: { /api: { target: http://localhost:8080, // ← 这里必须和你后端端口一致 changeOrigin: true, pathRewrite: { ^/api: // ← 把 /api 前缀去掉后端接口实际是 /login不是 /api/login } } }如果你后端改了端口比如改成 8090这里必须同步改target否则所有请求 404。这是新手最常翻车的第一步前后端端口不一致还傻等页面加载。3.2 登录态与路由守卫的底层逻辑登录成功后前端把 JWT 存进localStorage的tokenkey并通过router.beforeEach全局守卫校验// src/router/index.js router.beforeEach((to, from, next) { const token store.getters.token if (to.matched.some(m m.meta.requireAuth)) { // requireAuth 为 true 的路由需登录 if (token) { next() } else { next({ path: /login, query: { redirect: to.fullPath } }) } } else { next() } })注意store.getters.token实际读取的是localStorage.getItem(token)不是 Vuex state。所以你手动往 localStorage 写 token页面刷新后依然有效。但logout方法里只清了localStorage.removeItem(token)没调后端/api/logout接口——这是个设计选择它把登出视为纯前端行为Token 过期靠后端 JWT Filter 拦截。如果你要加服务端强制登出得在Logout.vue里补一个this.$axios.post(/api/logout)调用。3.3 Element UI 表单校验的隐藏坑日期组件与后端格式强绑定考勤系统大量用到日期选择器比如排班设置里的start_time和end_time。前端用的是el-date-pickertypedatetime但后端接收参数是String类型期望格式为yyyy-MM-dd HH:mm:ss。如果你在表单里选了2024-05-20 08:30提交时后端会收到2024-05-20T08:30:00.000ZISO 格式导致DateTimeParseException。解决方案在src/utils/request.js里已预埋// 请求拦截器对 date 类型字段做格式化 service.interceptors.request.use(config { if (config.data typeof config.data object) { Object.keys(config.data).forEach(key { if (config.data[key] instanceof Date) { config.data[key] moment(config.data[key]).format(YYYY-MM-DD HH:mm:ss) } }) } return config })但前提是你的表单数据必须是Date对象不是字符串。所以ScheduleForm.vue里必须这样写el-date-picker v-modelform.startTime !-- form.startTime 是 Date 类型 -- typedatetime value-formatyyyy-MM-dd HH:mm:ss !-- ← 这行必须加否则 v-model 绑定的是字符串 -- /漏掉value-formatv-model就是字符串拦截器不生效后端直接 400。这是 Vue 2 Element UI 的经典玄学坑血泪经验只要涉及时间value-format和v-model类型必须严格匹配。4. 核心业务闭环从打卡到统计一条数据如何贯穿前后端考勤系统的核心不是界面而是「打卡动作 → 状态判定 → 数据聚合」这条链路是否真实可靠。我们以「员工 A 在工位旁扫码打卡」为例拆解完整流程。4.1 打卡接口/api/record/checkin 的四层校验前端点击「打卡」按钮调用POST /api/record/checkin传参{ userId: 1001, latitude: 31.2304, longitude: 121.4737, deviceType: wechat, photo: data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/... }后端CheckInController.java会依次执行设备合法性校验deviceType必须是wechat、app或web否则返回400 Bad Request地理围栏校验查sys_dept表获取该员工所属部门的geo_fenceWKT 格式多边形用 MySQL 的ST_Contains判断(latitude, longitude)是否在范围内时间规则校验查att_schedule表获取今日排班判断当前时间是否在start_time到end_time的 15 分钟弹性区间内当日唯一性校验查att_record表WHERE user_id ? AND DATE(create_time) CURDATE()防止重复打卡。只有四层全过才插入新记录并返回{code: 200, msg: 打卡成功, data: {id: 12345}}。如果第 2 步失败返回{msg: 不在办公区域请移步至公司范围内打卡}如果第 3 步失败返回{msg: 未到打卡时间请于 08:45-09:00 之间打卡}。这种分层返回比笼统的「打卡失败」有用得多。4.2 排班表 att_schedule 的设计哲学支持「固定班次」与「轮班制」att_schedule表结构如下字段类型说明idBIGINT主键dept_idBIGINT部门 ID关联 sys_deptweek_dayTINYINT1-7表示周几1周一start_timeTIME开始时间如 09:00:00end_timeTIME结束时间如 18:00:00is_restTINYINT0工作日1休息日create_timeDATETIME创建时间关键点在于它不绑定具体员工而是按部门星期几配置。比如市场部dept_id3周一到周五is_rest0start_time09:00:00而技术部dept_id2实行大小周周一三五is_rest0二四is_rest1。员工打卡时后端根据user.dept_id和WEEKDAY(NOW())1查排班自动适配轮班逻辑。这种设计省去了给每个员工单独维护排班表的麻烦也方便 HR 一键调整整个部门的作息。4.3 缺勤统计报表/api/report/absence 的动态 SQL统计页面调用GET /api/report/absence?month2024-05后端AbsenceReportService.java生成动态 SQLSELECT u.user_name, u.phone, d.dept_name, COUNT(CASE WHEN r.status 0 THEN 1 END) AS normal_count, COUNT(CASE WHEN r.status 1 THEN 1 END) AS late_count, COUNT(CASE WHEN r.status 2 THEN 1 END) AS early_leave_count, COUNT(CASE WHEN r.status 3 THEN 1 END) AS absent_count FROM sys_user u LEFT JOIN sys_dept d ON u.dept_id d.id LEFT JOIN att_record r ON u.id r.user_id AND YEAR(r.create_time) 2024 AND MONTH(r.create_time) 5 GROUP BY u.id, u.user_name, u.phone, d.dept_name ORDER BY d.dept_name, u.user_name注意LEFT JOIN和COUNT(CASE WHEN...)的组合即使某员工当月没打卡记录也会出现在结果里normal_count等字段为 0。这是真实业务需求——HR 要看到「谁根本没打卡」而不是只统计有记录的人。如果用INNER JOIN缺勤者就直接消失了。5. 避坑指南五个让我重启三次才定位到的致命细节这些不是文档里写的“注意事项”而是我在 Windows 11 WSL2 MySQL 8.0 环境下反复重装、抓包、断点调试后记下的真实踩坑记录。每一条都对应一个让你卡住 2 小时以上的黑匣子。5.1 现象前端登录成功后跳转/dashboard页面空白控制台报TypeError: Cannot read property name of undefined原因src/store/modules/user.js里state.userInfo初始化为空对象{}但Dashboard.vue模板里直接用了{{ userInfo.name }}而登录成功后userInfo是异步赋值的mounted钩子执行时userInfo还是空对象。解决在Dashboard.vue的 template 里加 v-if 保护div v-ifuserInfo userInfo.name{{ userInfo.name }}/div或在computed里用this.$store.getters.userInfo?.name || -。5.2 现象MySQL 启动后att_record表里location字段显示为NULL即使打卡时传了经纬度原因application.yml里spring.jpa.hibernate.ddl-auto被设为updateHibernate 自动建表时把POINT类型映射成了VARCHAR(255)导致ST_PointFromText函数失效。解决把ddl-auto改为none彻底禁用 Hibernate 自动建表坚持用init.sql手动建表。这是地理信息系统的铁律空间字段绝不交给 ORM 自动生成。5.3 现象npm run dev启动后修改Login.vue保存页面不热更新必须手动刷新原因build/webpack.base.conf.js里module.rules的babel-loader配置漏了cacheDirectory: trueWebpack 4 的 watch 机制在某些文件系统尤其是 WSL2下失效。解决在babel-loader配置里加上cacheDirectory: true并确保node_modules不在 WSL2 的/mnt/c/路径下那是 Windows 文件系统性能极差。5.4 现象导出 Excel 功能点击无反应Network 面板看不到请求原因src/api/export.js里exportExcel方法用的是axios.get但后端/api/export/excel是PostMapping必须用 POST。解决把axios.get改成axios.post并把参数从 URL 查询参数改为 request body// 原来错误的写法 return axios.get(/api/export/excel?month${month}deptId${deptId}) // 正确写法 return axios.post(/api/export/excel, { month, deptId })5.5 现象微信扫码打卡时photo字段上传后后端att_record.photo字段存的是data:image/jpeg;base64,...字符串但前端展示时图片不显示原因MySQLTEXT字段默认最大长度 65535 字节而一张 1MB 的 base64 图片解码后约 1.3MB远超限制被 MySQL 自动截断导致 base64 不完整浏览器解析失败。解决修改att_record.photo字段类型为LONGTEXT并在application.yml的 JDBC URL 里加maxAllowedPacket6710886464MB。6. 进阶技巧用 Docker 一键部署 Nginx 反向代理告别环境焦虑当你确认本地能跑通下一步就是把它扔到服务器上让同事也能访问。别折腾java -jar和nginx.conf手动配——用 Docker Compose 三行命令搞定这才是工程师该有的效率。6.1 构建后端 Docker 镜像在backend目录下新建DockerfileFROM openjdk:11-jre-slim VOLUME /tmp ARG JAR_FILEtarget/attendance-system-0.0.1-SNAPSHOT.jar COPY ${JAR_FILE} app.jar ENTRYPOINT [java,-Djava.security.egdfile:/dev/./urandom,-jar,/app.jar]然后构建cd backend mvn clean package -Dmaven.test.skiptrue docker build -t attendance-backend .6.2 构建前端 Nginx 镜像在frontend目录下新建Dockerfile.nginxFROM nginx:alpine COPY dist/ /usr/share/nginx/html/ COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80先构建前端 distcd frontend npm run build # 生成 dist 目录 docker build -f Dockerfile.nginx -t attendance-frontend .6.3 编排 docker-compose.yml在项目根目录创建docker-compose.ymlversion: 3.8 services: db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: 123456 MYSQL_DATABASE: attendance volumes: - ./backend/src/main/resources/sql/init.sql:/docker-entrypoint-initdb.d/init.sql command: --default-authentication-pluginmysql_native_password backend: image: attendance-backend depends_on: - db environment: - SPRING_PROFILES_ACTIVEdocker - SPRING_DATASOURCE_URLjdbc:mysql://db:3306/attendance?useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/Shanghai - SPRING_DATASOURCE_USERNAMEroot - SPRING_DATASOURCE_PASSWORD123456 ports: - 8080:8080 frontend: image: attendance-frontend depends_on: - backend ports: - 80:80注意SPRING_PROFILES_ACTIVEdocker—— 你需要在backend/src/main/resources/下新建application-docker.yml把数据库和 Redis 配置全换成容器内网地址db、redis而不是localhost。6.4 一键启动与验证# 启动全部服务 docker-compose up -d # 查看日志确认无 ERROR docker-compose logs -f backend # 访问 http://your-server-ip应该看到登录页 # 后端 API 地址是 http://your-server-ip/api/user/listNginx 已反向代理Nginx 配置nginx.conf关键部分location /api/ { proxy_pass http://backend:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }这样前端所有/api/xxx请求都会被 Nginx 转发到backend:8080容器彻底解决跨域也不用改前端任何代码。从那以后我每次部署 JavaVue 项目都强制走一遍docker-compose up -d docker-compose logs -f看着backend容器日志里刷出Started AttendanceSystemApplication in X seconds才敢去泡杯咖啡。环境问题永远是最贵的调试成本而容器化就是我的后悔药。希望帮到你。本文还有配套的精品资源点击获取