news 2026/10/14 21:30:13

Spring Boot+Vue考勤系统实战:教务闭环与防代签设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot+Vue考勤系统实战:教务闭环与防代签设计

简介:本资源是一套完整的高校课堂考勤系统毕业设计级项目源码,面向Java Web与Vue全栈初学者及计算机专业本科生,解决传统人工考勤效率低、数据难统计、管理不透明等教学管理痛点。项目采用Spring Boot + Vue前后端分离架构,后端以146个Java类和23个XML配置文件构成清晰的SSM风格业务逻辑与数据访问层,前端含70个Vue组件及配套CSS/JS资源,辅以3个bat批处理脚本(install/run/build)实现一键部署,另含论文.doc与开题.docx两份完整文档支撑课程设计闭环。资源共492个文件,涵盖SVG图标、PNG/JPG素材、woff/ttf字体、SQL建表脚本及MP3/MP4多媒体示例,压缩包大小为17.63MB。目前已有51人学习下载,读者可直接运行调试、理解模块化考勤流程(如二维码签到、实时数据看板、导出统计报表),掌握企业级前后端协同开发规范与典型目录结构设计。

1. 这不是又一个“学生签到页面”:SSM686 是一套能跑通教务闭环的 Java Web + Vue 考勤系统实战包

某高校信息中心曾反馈:上线过 3 套前端花哨、后台空转的“考勤系统”,教师扫完码,数据进不了教务平台;学生点完“已到”,导出 Excel 却缺课时字段;管理员想查某班上周缺勤 Top5,SQL 写到第三层 JOIN 就报错。这不是功能堆砌的问题,是架构断层——Java 后端没暴露标准 REST 接口,Vue 前端没做状态持久化,数据库没建好考勤快照表。而这份名为ssm686的资源包,恰恰卡在断层最痛的位置:它用 Spring Boot(非老式 SSM,标题中 SSM 是历史命名惯性)重写了后端服务层,Vue 2.6+ 实现了带离线缓存的课堂扫码页,MySQL 表结构里明确定义了attendance_snapshot和course_session_log两张核心表。它不承诺“一键部署全校”,但能让你在本地 Docker 环境里,15 分钟内跑通“教师创建课节 → 学生扫码签到 → 后台导出带学号/姓名/时间戳/设备指纹的考勤明细表”全链路。适合正在做课程设计、毕设答辩前两周还在调跨域、或需要快速交付院系级轻量考勤模块的 Java/Vue 开发者。


2. 从解压到首屏:环境准备、目录结构与启动验证三步闭环

2.1 环境依赖清单:别被“Java Web”字面意思带偏

这个包本质是Spring Boot 2.3.12 + Vue CLI 3.12 + MySQL 5.7的组合,不是传统 WAR 包部署模式。这意味着你不需要 Tomcat 容器,也不需要手动配置 web.xml。但必须确认以下三项:

  • JDK 版本:JDK 1.8.0_291 或更高(必须是 8u291+,低版本会因 Spring Boot 2.3 对 Jakarta EE 的迁移报javax.servlet类找不到)
  • Node.js:v14.17.0(Vue CLI 3.12 的硬性要求,v16+ 会导致vue-cli-service serve编译失败)
  • MySQL:5.7.33(注意:8.0+ 默认开启caching_sha2_password认证插件,而包内application.yml用的是mysql-connector-java:8.0.22,需手动改authenticationPlugin配置)

提示:不要用java -version看到 “1.8.0” 就以为过关——执行java -version后检查输出末尾是否含291或更高小版本号;Node.js 同理,node -v必须精确到v14.17.0,用nvm切换比全局安装更稳妥。

2.2 解压后直击核心:识别四个关键目录与作用

解压ssm686.rar后,你会看到如下结构(已剔除.git、target、node_modules等无关目录):

ssm686/ ├── backend/ # Spring Boot 工程根目录(Maven 结构) │ ├── pom.xml # 关键:确认 <spring-boot.version>2.3.12.RELEASE</spring-boot.version> │ └── src/main/ │ └── java/com/example/ssm686/ # 主包名,Controller/Service/Entity 全在此 ├── frontend/ # Vue CLI 工程根目录 │ ├── package.json # 关键:"vue": "^2.6.14", "vue-cli-service": "^3.12.1" │ └── src/ │ ├── api/ # 所有 axios 请求封装,如 attendance.js 定义 /api/attendance/checkin │ └── router/ # 路由配置,重点看 index.js 中 '/teacher' 和 '/student' 的权限守卫 ├── docs/ # 数据库脚本与接口文档(非 PDF,是 .sql 和 .md) │ ├── ssm686.sql # 建库建表 SQL(含初始化教师/学生账号) │ └── api_ref.md # 每个接口的 request body 示例与 response 字段说明 └── README.md # 仅两行:“后端:cd backend && mvn spring-boot:run”、“前端:cd frontend && npm run serve”

注意:backend/src/main/resources/application.yml是配置中枢,其中spring.datasource.url默认为jdbc:mysql://localhost:3306/ssm686?useSSL=false&serverTimezone=Asia/Shanghai—— 若你 MySQL 端口不是 3306,或数据库名要改,必须先改这里,再执行 SQL 脚本,否则启动直接报Unknown database。

2.3 启动验证:三步确认服务真正就绪(不是“控制台没报错”就算成功)

步骤一:初始化数据库
# 在 MySQL 客户端执行(确保已创建空库 ssm686) mysql -u root -p ssm686 < docs/ssm686.sql

该 SQL 脚本会创建 6 张表,重点关注:

  • t_user:role字段值为TEACHER或STUDENT,初始账号密码均为123456
  • t_course_session:课节表,status字段控制是否可签到(OPEN=可扫,CLOSED=关闭)
  • t_attendance_record:签到主表,device_fingerprint字段存扫码设备唯一标识(非 IP!是前端生成的 UUID)
步骤二:启动后端(关键验证点)
cd backend mvn clean compile spring-boot:run

等待日志出现:

Tomcat started on port(s): 8080 (http) with context path '' Started Ssm686Application in 8.2 seconds (JVM running for 9.1)

此时访问http://localhost:8080/api/test(任意未鉴权接口),应返回 JSON{"code":200,"msg":"ok","data":null}。若返回 404,说明@RestController扫描路径不对,检查Ssm686Application.java上的@SpringBootApplication(scanBasePackages = "com.example.ssm686")是否匹配实际包名。

步骤三:启动前端并登录
cd frontend npm install # 首次运行必须,装完约 3 分钟 npm run serve

浏览器打开http://localhost:8081(Vue CLI 默认端口),输入teacher/123456登录。验证成功标志:左上角显示“教师工作台”,点击【新建课节】弹窗能正常加载课程下拉框(数据来自/api/course/list接口)。若下拉框为空,打开浏览器开发者工具 → Network 标签,筛选 XHR,看/api/course/list是否返回 200 且data数组非空 —— 这是前后端连通性的黄金指标。


3. 教师端核心流程:课节创建、动态二维码生成与实时签到监控

3.1 创建课节:为什么t_course_session表必须有start_time和end_time?

在教师工作台点击【新建课节】,表单包含:课程名称、班级、开始时间、结束时间、最大签到人数。提交后,后端CourseSessionController.create()方法会插入一条记录到t_course_session。关键逻辑不在前端校验,而在后端:

// backend/src/main/java/com/example/ssm686/controller/CourseSessionController.java @PostMapping("/create") public Result create(@RequestBody CourseSession session) { // 1. 强制设置 status = OPEN(只有 OPEN 状态才允许扫码) session.setStatus("OPEN"); // 2. 设置当前时间作为创建时间 session.setCreateTime(new Date()); // 3. 校验:结束时间必须晚于开始时间,且不能早于当前时间 if (session.getEndTime().before(session.getStartTime()) || session.getStartTime().before(new Date())) { return Result.fail("课节时间设置错误"); } courseSessionService.save(session); return Result.success(); }

参数说明:start_time和end_time不仅用于界面展示,更是签到逻辑的闸门。AttendanceService.checkIn()方法在扫码时会校验:now >= start_time && now <= end_time,否则返回{"code":403,"msg":"课节未开始或已结束"}。这是防止学生提前扫、课后补扫的核心防线,不能靠前端 JS 时间校验,必须后端强控。

3.2 动态二维码生成:不是简单 base64,而是带签名的短链

学生扫码页 URL 并非http://localhost:8081/student/checkin?sessionId=123这种明文 ID,而是形如https://s686.io/q/abc123的短链。其生成逻辑在QrCodeService.generateQrCode(Long sessionId):

// backend/src/main/java/com/example/ssm686/service/QrCodeService.java public String generateQrCode(Long sessionId) { // 1. 生成 6 位随机字符串(非 UUID,避免过长) String shortCode = RandomStringUtils.randomAlphanumeric(6).toLowerCase(); // 2. 存入 t_qr_code_mapping 表,关联 sessionId 和 shortCode QrCodeMapping mapping = new QrCodeMapping(); mapping.setShortCode(shortCode); mapping.setSessionId(sessionId); mapping.setExpireTime(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000)); // 24小时过期 qrCodeMappingMapper.insert(mapping); // 3. 返回短链(实际项目中此处应调用 Nginx 302 跳转,本包用前端路由模拟) return "https://s686.io/q/" + shortCode; }

逻辑说明:短链设计解决三个问题:① 避免 sessionId 泄露数据库主键规律;② 支持过期控制(expireTime字段);③ 为后续接入真实短链服务(如自建 or 使用开源 go-url)留扩展点。前端student/checkin.vue通过this.$route.params.code获取abc123,再调用/api/qr/resolve?code=abc123接口反查出真实sessionId,这才进入签到主流程。

3.3 实时签到监控:WebSocket 不是噱头,是解决“教师想知道谁还没来”的刚需

教师工作台右上角有【实时签到】面板,显示当前课节已签到人数、未签到名单(按学号排序)。这并非轮询实现,而是基于 Spring Boot WebSocket:

// backend/src/main/java/com/example/ssm686/config/WebSocketConfig.java @Configuration @EnableWebSocket public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } } // backend/src/main/java/com/example/ssm686/websocket/AttendanceWebSocket.java @ServerEndpoint("/ws/attendance/{sessionId}") @Component public class AttendanceWebSocket { private static final Map<Long, Set<Session>> SESSIONS = new ConcurrentHashMap<>(); @OnOpen public void onOpen(Session session, @PathParam("sessionId") Long sessionId) { SESSIONS.computeIfAbsent(sessionId, k -> ConcurrentHashMap.newKeySet()).add(session); } // 当学生签到成功,触发此广播 public static void broadcastSignin(Long sessionId, String studentNo) { Set<Session> sessions = SESSIONS.get(sessionId); if (sessions != null) { sessions.forEach(s -> { try { s.getBasicRemote().sendText( JSON.toJSONString(Map.of("type", "SIGNIN", "studentNo", studentNo)) ); } catch (IOException e) { // 移除异常连接 sessions.remove(s); } }); } } }

参数说明:前端教师页通过new WebSocket("ws://localhost:8080/ws/attendance/123")连接,监听message事件。当收到{type:"SIGNIN", studentNo:"2021001"},就将该学号从“未签到列表”移至“已签到列表”。注意:WebSocket 路径中的sessionId必须与当前教师查看的课节 ID 一致,否则收不到消息—— 这是新手最容易忽略的绑定逻辑。


4. 学生端扫码签到:离线缓存、设备指纹与防代签三重校验

4.1 离线缓存策略:Vue PWA 模式下,扫码页为何能断网使用?

frontend/vue.config.js中启用了 PWA 插件:

// frontend/vue.config.js module.exports = { pwa: { workboxOptions: { skipWaiting: true, clientsClaim: true, runtimeCaching: [ { urlPattern: /^https:\/\/.*\/api\/.*$/, handler: 'NetworkFirst', // API 请求走网络 options: { cacheName: 'api-cache' } }, { urlPattern: /\.(?:png|jpg|jpeg|gif|ico|svg|webp|woff2|ttf|css|js)$/, handler: 'CacheFirst', // 静态资源缓存 options: { cacheName: 'static-cache' } } ] } } }

逻辑说明:学生首次访问http://localhost:8081/student/checkin时,Service Worker 会缓存 HTML、JS、CSS 及二维码图片。当网络中断,刷新页面仍能显示扫码界面(因为 HTML/JS 已缓存),但扫码动作本身会失败 —— 因为/api/attendance/checkin接口必须联网调用。真正的离线价值在于:学生可提前打开页面,等教师开课后立即扫码,避免网络波动导致首屏加载慢而错过签到窗口。这不是“完全离线”,而是“首屏离线 + 业务操作强联网”。

4.2 设备指纹生成:为什么不用 IP?如何防同一手机多次签到?

学生扫码后,前端attendance.js发送请求:

// frontend/src/api/attendance.js export function checkIn(sessionId, studentNo) { const fingerprint = getDeviceFingerprint(); // 关键函数 return request({ url: '/api/attendance/checkin', method: 'post', data: { sessionId, studentNo, deviceFingerprint: fingerprint } }) } function getDeviceFingerprint() { // 组合:UserAgent + 屏幕分辨率 + 语言 + 时区 + Canvas 渲染哈希 const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); ctx.textBaseline = 'top'; ctx.font = '14px Arial'; ctx.textRendering = 'optimizeLegibility'; ctx.fillText('ssm686', 2, 2); const canvasHash = canvas.toDataURL().slice(-10); // 取 DataURL 末10位作简易哈希 return btoa(navigator.userAgent + screen.width + screen.height + navigator.language + Intl.DateTimeFormat().resolvedOptions().timeZone + canvasHash).substring(0, 32); // Base64 后截取32位 }

参数说明:deviceFingerprint字段存入t_attendance_record.device_fingerprint。后端AttendanceService.checkIn()会校验:同一sessionId下,该指纹是否已存在记录。若存在,返回{"code":409,"msg":"该设备已签到"}。这比单纯校验studentNo更可靠—— 防止学生用多台手机帮同学代扫(每台手机指纹不同),也避免同一手机切号重复签(指纹不变,系统拦截)。

4.3 防代签增强:地理位置与拍照水印(可选启用)

包内frontend/src/utils/geolocation.js提供了地理围栏能力(默认关闭):

// frontend/src/utils/geolocation.js export function getLocation() { return new Promise((resolve, reject) => { if (!navigator.geolocation) { reject('Geolocation not supported'); return; } navigator.geolocation.getCurrentPosition( position => resolve({ lat: position.coords.latitude, lng: position.coords.longitude, accuracy: position.coords.accuracy }), err => reject(err.message), { enableHighAccuracy: true, timeout: 5000, maximumAge: 30000 } ); }); }

逻辑说明:若教师在创建课节时勾选“启用位置校验”,则后端checkIn()会调用高德/百度地图 API(需自行填入 key),判断学生上报的经纬度是否在教室坐标 50 米范围内。同时,student/checkin.vue中的takePhoto()方法会调用navigator.mediaDevices.getUserMedia拍摄一张带时间戳、学号水印的现场照片(base64 存入t_attendance_record.photo_data)。这两项是“玄学级防代签”手段,不强制开启,但代码已预留钩子—— 你需要做的只是在application.yml中配置location.enabled=true和photo.enabled=true。


5. 避坑指南:五个血泪经验总结的高频翻车点与排查路径

5.1 现象:前端登录后跳转 404,地址栏变成http://localhost:8081/#/teacher/dashboard但页面空白

原因:Vue Router 模式为history,但开发服务器npm run serve默认不支持 history fallback,导致刷新页面时 Nginx/Apache 无法找到对应 HTML。
解决:打开frontend/vue.config.js,确认devServer.historyApiFallback已启用:

devServer: { historyApiFallback: true, // 必须为 true port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }

若已启用仍无效,清空浏览器缓存并重启npm run serve。

5.2 现象:教师创建课节成功,但学生扫码提示{"code":404,"msg":"课节不存在"}

原因:t_qr_code_mapping表中shortCode与sessionId关联失败,常见于QrCodeService.generateQrCode()中qrCodeMappingMapper.insert(mapping)抛异常但未被捕获(如数据库字段长度不足)。
解决:检查t_qr_code_mapping.short_code字段类型是否为VARCHAR(10)(脚本中定义为 10,若手动改过表结构需同步);在generateQrCode方法开头加日志log.info("Generating QR for session: {}", sessionId),确认方法是否执行;查询SELECT * FROM t_qr_code_mapping WHERE short_code = 'abc123'验证记录是否存在。

5.3 现象:WebSocket 连接成功,但教师页收不到签到消息

原因:AttendanceWebSocket.broadcastSignin()调用时机错误。该方法应在AttendanceService.checkIn()事务提交后触发,而非事务内调用(否则事务回滚会导致消息已发但签到失败)。
解决:检查AttendanceServiceImpl.checkIn()方法,确认broadcastSignin()是否在attendanceRecordMapper.insert(record)之后,且不在@Transactional方法内部直接调用。正确做法是使用 Spring 事件机制或异步线程池:

// 在 checkIn 方法末尾 applicationEventPublisher.publishEvent(new AttendanceSigninEvent(sessionId, studentNo)); // 新建监听器类处理广播 @Component public class AttendanceSigninListener implements ApplicationListener<AttendanceSigninEvent> { @Override public void onApplicationEvent(AttendanceSigninEvent event) { AttendanceWebSocket.broadcastSignin(event.getSessionId(), event.getStudentNo()); } }

5.4 现象:导出 Excel 明细表,时间字段全是1970-01-01

原因:t_attendance_record.create_time字段类型为BIGINT(存毫秒时间戳),但 EasyExcel 导出时未做Date类型转换,直接 toString() 导致解析失败。
解决:检查backend/src/main/java/com/example/ssm686/excel/AttendanceExport.java,确认@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")注解是否加在createTime字段上:

public class AttendanceExport { @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date createTime; // 必须是 Date 类型,非 Long }

同时,在AttendanceController.export()中,将数据库查出的Long createTime赋值给AttendanceExport.createTime时,需显式转换:export.setCreateTime(new Date(record.getCreateTime()))。

5.5 现象:学生扫码后,t_attendance_record表新增记录,但t_user.last_login_time未更新

原因:AttendanceService.checkIn()中只操作了考勤表,未联动更新用户表。而教师端“最近活跃学生”统计依赖last_login_time。
解决:在checkIn()方法中,扫码成功后追加更新语句:

// 更新用户最后登录时间(提升教师端数据新鲜度) LambdaUpdateWrapper<User> userWrapper = new LambdaUpdateWrapper<>(); userWrapper.eq(User::getStudentNo, studentNo) .set(User::getLastLoginTime, new Date()); userMapper.update(null, userWrapper);

注意:此操作需在同一个事务中,确保原子性。


6. 进阶技巧:用 Docker Compose 一键拉起全栈环境与生产化部署 checklist

6.1 Docker Compose 编排:三行命令启动 MySQL + Backend + Frontend

包内未提供 Dockerfile,但根据技术栈可快速补全。在项目根目录新建docker-compose.yml:

version: '3.8' services: mysql: image: mysql:5.7.33 container_name: ssm686-mysql environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: ssm686 ports: - "3306:3306" volumes: - ./docs/ssm686.sql:/docker-entrypoint-initdb.d/init.sql - mysql-data:/var/lib/mysql backend: build: ./backend container_name: ssm686-backend ports: - "8080:8080" environment: SPRING_PROFILES_ACTIVE: docker SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/ssm686?useSSL=false&serverTimezone=Asia/Shanghai depends_on: - mysql frontend: build: ./frontend container_name: ssm686-frontend ports: - "8081:80" environment: VUE_APP_API_BASE_URL: http://localhost:8080 depends_on: - backend volumes: mysql-data:

关键步骤:

  1. 在backend/src/main/resources/下新建application-docker.yml,内容为spring.datasource.url指向mysql服务名;
  2. frontend/Dockerfile使用nginx:alpine,COPY dist/ /usr/share/nginx/html/;
  3. 执行docker-compose up -d --build,等待 2 分钟,访问http://localhost:8081即可。
    优势:彻底隔离本地环境,避免 JDK/Node 版本冲突;MySQL 初始化脚本自动执行;服务依赖关系清晰。

6.2 生产化部署 checklist:从“能跑”到“能扛住”的七项加固

项目检查点说明是否完成
HTTPS 强制Nginx 配置return 301 https://$host$request_uri;前端所有请求必须走 HTTPS,防止deviceFingerprint被中间人篡改□
JWT 替换 Sessionbackend/pom.xml加入jjwt-api、jjwt-impl当前用HttpSession,并发量大时内存溢出;JWT 无状态,适合横向扩展□
敏感配置外置application.yml中spring.datasource.password改为${DB_PASSWORD},启动时传-e DB_PASSWORD=xxx避免密码硬编码在 Git 仓库□
日志脱敏logback-spring.xml中<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>改为过滤studentNo、idCard字段防止日志泄露学生隐私□
SQL 注入防护检查所有@Select("SELECT * FROM t_user WHERE student_no = #{studentNo}")是否全为#{}(预编译),禁用${}${}直接拼接,高危!□
前端资源压缩frontend/vue.config.js中configureWebpack.optimization.minimize = true减少 JS/CSS 体积,提升首屏速度□
健康检查端点backend/src/main/java/com/example/ssm686/controller/HealthController.java添加@GetMapping("/actuator/health")供 Kubernetes/LVS 做存活探针□

从那以后我每次接手新项目,都强制走一遍这个 checklist —— 不是怕线上崩,是怕凌晨三点被电话叫醒时,发现崩的原因是application.yml里还写着root/root。把配置、日志、安全这些“无聊事”做扎实,才能腾出手去优化那个让教师拍手叫好的实时签到动画。希望帮到你。

本文还有配套的精品资源,点击获取

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

C#上位机权限管理实战:RBAC模型、登录认证与界面联动控制

简介&#xff1a;AuthDemo是一份面向C#上位机开发者的权限管理示例项目&#xff0c;专门解决工业控制、设备监控类软件中用户身份认证、角色配置、功能模块访问控制等安全需求。项目从登录界面入手&#xff0c;演示了用户名密码输入、多因素认证扩展&#xff08;如短信验证码、…

作者头像 李华
网站建设 2026/10/14 21:29:51

CGCS2000县级矢量数据:空间分析的可靠几何基底

简介&#xff1a;本资源是一套覆盖全国范围的标准化GIS矢量数据集&#xff0c;面向地理信息、城乡规划、交通水利、环境研究及高校GIS教学等领域的从业者与学习者&#xff0c;解决基础空间底图缺失、多源要素整合困难等问题。数据包含县级与地州级行政边界、长江黄河等主要河流…

作者头像 李华
网站建设 2026/10/14 21:28:56

Agentic RAG实战:多跳问答中的检索决策循环与源码解析

简介&#xff1a;Agentic RAG即代理式检索增强生成&#xff0c;本资源以可运行源码形式系统解析其核心原理与工程实现&#xff0c;适合已掌握基础RAG概念、希望进阶学习AI Agent与检索系统结合的开发者。内容重点对比传统RAG的单一知识源与一次性检索局限&#xff0c;展示如何借…

作者头像 李华
网站建设 2026/10/14 21:28:12

在Hive上构建MOLAP:预聚合加速大数据多维分析

1. 先聊透&#xff1a;Hive上做MOLAP到底解决什么问题第一次听到"在Hive上做MOLAP"这个说法时&#xff0c;我的第一反应也是愣了一下。MOLAP是传统的多维数据库玩法&#xff0c;Hive是跑离线大数据SQL的数据仓库工具&#xff0c;两者给人的印象差着辈分。但认真做完一…

作者头像 李华