ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于SpringBoot+Vue的WIFI指纹课堂点名系统设计与实现

基于SpringBoot+Vue的WIFI指纹课堂点名系统设计与实现 1. 先说清楚为什么点名系统要选WIFI协议而不是二维码、GPS或蓝牙做过课堂签到类项目的人应该都遇到过同一个尴尬老师辛辛苦苦发起一次点名结果同学们在宿舍床上就把签到了。原因很简单不管是二维码、动态验证码还是定位签到都存在一个核心漏洞——签到凭据可以被人转发位置判断又不够精准。我最初做这个基于javaspringbootvue的课堂点名系统时也先考虑过二维码方案。老师在大屏上放出二维码学生扫码即签到听起来挺顺。但实际用起来问题很大二维码截个图发到班级群里全宿舍瞬间到齐而且扫码动作本身只证明手机扫了码证明不了人坐在教室里。GPS定位虽然能区分大概位置但室内精度是灾难级的教室和隔壁教室都分不清更别说教学楼上下层。后来换了思路把目光放到WIFI协议上。教室基本都有无线APAccess Point无线接入点覆盖学生手机日常连着教室WIFI或者至少能扫描到教室周围的WIFI信号这本身就是天然的在场证据。通过采集设备周围的WIFI信号列表、信号强度RSSI和接入点MAC地址BSSID后端就能判断这个设备大概率在哪个位置。这个方案的好处是不需要额外硬件复用学校/教室已有的WIFI设施信号指纹和课程表绑定只有物理位置正确才能签到可伪造难度比二维码高得多代签成本大幅提高这篇文章我就把这个系统的完整实现思路拆开讲一遍从WIFI指纹判定的核心原理到SpringBoot后端接口设计、Vue前端页面再到数据库表结构和毕设答辩要准备的东西全流程过一遍。适合正在做Java毕设、想搞一个有技术亮点的签到类项目的同学参考。2. 技术选型为什么是SpringBootVue这套组合的边界在哪2.1 后端选型SpringBoot 3还是2.7先解决一个很多人纠结的问题。SpringBoot 3.x要求JDK 17SpringBoot 2.7用JDK 8就行。我实际开发用的是JDK 8 SpringBoot 2.7.18理由是毕设部署环境往往比较随意有的同学本机就是JDK 8学校服务器也可能没有新版本JDK。加上2.7版本稳定、网上资料最多遇到问题好排查。如果你本机已经装了JDK 17用SpringBoot 3.x也没问题但注意3.x里面javax包改成了jakarta这个坑会浪费你不少时间。技术栈大体包括模块选型说明核心框架SpringBoot 2.7.18内嵌Tomcat可打jar包直接跑ORMMyBatis-Plus 3.5.x比JPA直观生成单表CRUD方便权限认证JWT 拦截器无状态认证自适应前后端分离数据库MySQL 8.0存储用户、课程、点名任务、考勤记录实时推送WebSocket教师端大屏实时刷新签到状态前端框架Vue 3 Vite 3组合式API开发体验好UI组件Element Plus表格、表单、弹窗开箱即用图表ECharts 5统计图表展示出勤率这套选型的逻辑是每个组件都尽量贴近当前主流课程设计和面试的技术栈。比如MyBatis-Plus虽然简化了SQL但答辩时你要能讲清底层是怎么拼接SQL的JWT要能讲清token过期处理和拦截器校验流程。2.2 前端选型Vue 3组合式API写起来更顺手前端为什么用Vue 3而不是Vue 2一方面Vue 3已经是绝对主流另一方面组合式APIscript setup写业务代码确实比Options API清晰。点名系统的前端分成两个角色页面教师端管理课程、创建点名任务、查看实时签到大屏、导出考勤表学生端查看课程列表、进入签到页面、确认提交WIFI信号学生端我用的是移动端Web页面没有单独做App。原因是一个页面同时兼容PC和手机浏览器开发成本低演示也方便。navigator.wifi这类API现在主流浏览器基本不支持了我们实际通过调用后端收集周围WIFI扫描结果的逻辑需要依赖HttpClient请求具体后面讲。2.3 这套方案的边界需要提前说明基于WIFI协议的课堂点名本质上解决的是设备位置判定问题不是身份唯一性问题。手机可以交给别人带到教室这个任何无感签到方案都绕不开除非上人脸识别。所以系统的设计目标是把代签门槛提高做到凭据不可转发、位置必须精准、行为可以审计在这一层上WIFI指纹是性价比极高的选择。3. WIFI指纹点名核心原理从信号强度到在不在教室3.1 为什么信号强度能定位WIFI信号强度RSSIReceived Signal Strength Indicator的单位是dBm值越大表示信号越强。常规的路由器信号强度在-30dBm贴着路由器到-90dBm很弱但能连之间。无线信号在室内传播时遵循对数路径损耗模型[ P_r(d) P_t - 10n \lg(d/d_0) X_g ]简化理解距离越近RSSI越高但中间隔了墙、人、金属柜子信号衰减会明显增大。所以单个AP的RSSI只能估算距离不能精确定位但多个AP的RSSI组合起来就形成了这个位置独有的信号指纹。举个例子教室A讲台上方的AP1信号强度高教室后门附近的AP2信号弱而隔壁教室B恰好相反。两个位置看到的信号列表和强度分布完全不一样只要把这个分布记录下来就能反过来判断设备在哪里。3.2 指纹库是怎么构建的系统前期需要在每个目标教室做一次指纹采集这是整个项目的数据地基。采集方法在教室选5~8个采样点讲台、第一排左中右、中间排、后排、门口每个点用手机或电脑扫描周围的WIFI列表记录AP的SSID网络名称、BSSIDAP的MAC地址、RSSI信号强度每个点连续采样10次去掉最大值和最小值取平均值降低信号抖动影响把多个采样点汇总形成教室指纹库存到数据库表的简化设计长这样CREATE TABLE wifi_fingerprint ( id bigint NOT NULL AUTO_INCREMENT, classroom_id bigint NOT NULL COMMENT 教室ID, bssid varchar(64) NOT NULL COMMENT AP MAC地址, ssid varchar(128) DEFAULT NULL COMMENT AP名称, avg_rssi int NOT NULL COMMENT 平均信号强度, sample_count int DEFAULT 1 COMMENT 采样次数, create_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_classroom (classroom_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;实测下来一个普通教室大约能采集到5~10个AP包括隔壁教室泄漏过来的信号这个量级足以区分不同教室。3.3 判定算法加权相似度匹配学生端上报的是当前设备扫描到的WIFI列表格式是[ {bssid: a4:2b:8c:xx:xx:01, ssid: TECH-EDU-5G, rssi: -45}, {bssid: a4:2b:8c:xx:xx:02, ssid: TECH-EDU-2.4G, rssi: -58}, {bssid: e0:cc:7a:xx:xx:f0, ssid: CMCC-EDU, rssi: -72} ]后端拿到数据之后跟目标教室的指纹库做相似度匹配。算法我用的是交集AP数 加权RSSI距离计算上报列表与指纹库的交集AP数量按BSSID匹配交集AP数量小于阈值比如3个直接判为不在教室对交集AP计算RSSI差值差值越小越相似最终得分 交集AP占比 × 权重 信号距离得分 × 权重核心代码public MatchResult matchWifiFingerprint(ListWifiSignal reported, ListWifiSignal fingerprint) { // 按bssid建立指纹库映射 MapString, Integer fpMap new HashMap(); for (WifiSignal fp : fingerprint) { fpMap.put(fp.getBssid(), fp.getRssi()); } int intersectCount 0; double totalRssiDistance 0.0; for (WifiSignal s : reported) { Integer fpRssi fpMap.get(s.getBssid()); if (fpRssi ! null) { intersectCount; totalRssiDistance Math.abs(s.getRssi() - fpRssi); } } int minIntersect 3; // 最少匹配AP数 if (intersectCount minIntersect) { return MatchResult.notMatched(匹配AP数不足); } double avgDistance totalRssiDistance / intersectCount; double threshold 12.0; // 平均RSSI差距阈值单位dBm boolean matched avgDistance threshold; return new MatchResult(matched, intersectCount, avgDistance); }阈值12dBm不是拍脑袋定的。我实测教室A和教室B之间的平均RSSI差距通常在20dBm以上同一个位置多次采样的波动在5~8dBm左右所以12dBm能在容忍波动和区分相邻教室之间达到平衡。具体数值需要根据你的教室环境微调建议在管理后台留一个参数配置入口。3.4 信号漂移怎么处理RSSI天然不稳定人走动、门开关、下雨都会影响。实际使用中我做了三层兜底采样点覆盖教室四角和中心指纹库本身就是一段信号区间而非单点判定时取多次上报的平均值如果一次点名允许学生重试把两次结果做平均再判维护一个信号校准功能课前任课老师用手机现场采样几分钟系统用实时均值替换或修正指纹库4. 系统功能拆解与数据库设计4.1 用户角色与功能模块系统涉及三种角色功能边界划清角色功能点管理员管理教师/学生账号、管理教室、维护WIFI指纹库、查看全局考勤统计教师管理课程、创建点名任务、查看实时签到看板、导出考勤Excel、发起补签审批学生查看今日课程、进入WIFI点名页、提交信号、查看个人考勤记录没有单独做管理员页面的毕设很常见但加上之后系统完整性会高很多答辩时也有更多内容可讲。4.2 数据库表结构核心表一共7张我把关键字段列出来user用户表CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, student_no varchar(32) DEFAULT NULL COMMENT 学号/工号, username varchar(64) NOT NULL, password varchar(128) NOT NULL COMMENT BCrypt加密, real_name varchar(64) NOT NULL, role tinyint NOT NULL DEFAULT 2 COMMENT 1管理员 2教师 3学生, class_name varchar(64) DEFAULT NULL COMMENT 学生所在班级, phone varchar(20) DEFAULT NULL, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;course课程表CREATE TABLE course ( id bigint NOT NULL AUTO_INCREMENT, course_name varchar(128) NOT NULL, teacher_id bigint NOT NULL COMMENT 教师用户ID, classroom_id bigint DEFAULT NULL COMMENT 教室ID, weekday tinyint DEFAULT NULL COMMENT 周几1-7, start_time time DEFAULT NULL, end_time time DEFAULT NULL, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;course_student选课关系表course_id student_id create_time用于确定某门课有哪些学生要签到。sign_task点名任务表CREATE TABLE sign_task ( id bigint NOT NULL AUTO_INCREMENT, course_id bigint NOT NULL, teacher_id bigint NOT NULL, classroom_id bigint NOT NULL, task_no varchar(32) NOT NULL COMMENT 点名码如SIGN-8A3F, status tinyint NOT NULL DEFAULT 0 COMMENT 0进行中 1结束, start_time datetime NOT NULL, end_time datetime DEFAULT NULL, duration_minutes int DEFAULT 5 COMMENT 点名窗口时长, PRIMARY KEY (id), UNIQUE KEY uk_task_no (task_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;sign_record签到记录表CREATE TABLE sign_record ( id bigint NOT NULL AUTO_INCREMENT, task_id bigint NOT NULL, student_id bigint NOT NULL, course_id bigint NOT NULL, sign_status tinyint NOT NULL DEFAULT 0 COMMENT 1正常 2迟到 3缺勤 4补签, wifi_report_json text COMMENT 上报的原始WIFI数据审计用, match_result tinyint DEFAULT 0 COMMENT 0不匹配 1匹配, device_id varchar(128) DEFAULT NULL COMMENT 设备指纹, sign_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_task_student (task_id, student_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有一个非常关键的细节uk_task_student这个唯一索引在数据库层面杜绝了同一学生对同一任务重复签到的可能。即使并发请求打进来也只有一个能成功插入。classroom教室表id、building、floor、room_no、longitude、latitude预留、description。wifi_fingerprintWIFI指纹表前面已经给了建表SQL。4.3 接口设计统一约束前后端分离最怕接口格式各写各的。我统一封装了返回体Data public class ResultT { private Integer code; private String msg; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.msg success; r.data data; return r; } public static T ResultT error(String msg) { ResultT r new Result(); r.code 500; r.msg msg; return r; } }核心接口清单方法路径说明POST/api/auth/login登录返回JWT tokenGET/api/course/myCourses当前用户课程列表按角色区分POST/api/sign/task/create教师创建点名任务GET/api/sign/task/current查询当前进行中的点名任务POST/api/sign/report学生上报WIFI信号列表GET/api/sign/records?taskId教师查看某任务签到情况GET/api/sign/statistics全局考勤统计GET/ws/sign/push/{taskId}WebSocket连接实时推送签到事件5. 后端核心模块实现点名任务、WIFI上报与防代签防线5.1 创建点名任务一次性令牌设计教师点击开始点名后端做的事不是一个INSERT那么简单。关键逻辑PostMapping(/sign/task/create) public ResultSignTaskVO createTask(RequestBody CreateTaskRequest req) { // 1. 校验当前课程是否已有进行中的点名任务 LambdaQueryWrapperSignTask wrapper new LambdaQueryWrapper(); wrapper.eq(SignTask::getCourseId, req.getCourseId()) .eq(SignTask::getStatus, 0); if (signTaskMapper.selectCount(wrapper) 0) { return Result.error(该课程已有进行中的点名任务); } // 2. 生成唯一点名码 String taskNo SIGN- RandomUtil.randomString(4).toUpperCase(); // 3. 计算结束时间 LocalDateTime now LocalDateTime.now(); LocalDateTime endTime now.plusMinutes(req.getDurationMinutes()); // 4. 插入任务 SignTask task new SignTask(); task.setCourseId(req.getCourseId()); task.setTeacherId(CurrentUser.getId()); task.setClassroomId(req.getClassroomId()); task.setTaskNo(taskNo); task.setStatus(0); task.setStartTime(now); task.setEndTime(endTime); signTaskMapper.insert(task); // 5. 异步启动一个延时任务到期自动关闭点名 scheduleService.closeTaskAfter(task.getId(), req.getDurationMinutes()); return Result.success(SignTaskVO.from(task)); }为什么要点名码一方面学生端需要在页面输入或扫码确认自己参加的是哪场点名另一方面它作为一个一次性令牌和JWT不同点名码只在特定时间窗口内有效过期自动作废增强了安全性。5.2 WIFI上报接口参数校验是关键学生端上报接口的完整处理链PostMapping(/sign/report) public ResultSignResultVO report(RequestBody WifiReportRequest req) { // 1. 校验任务是否在有效时间窗口内 SignTask task signTaskMapper.selectById(req.getTaskId()); if (task null || task.getStatus() ! 0) { return Result.error(点名任务不存在或已结束); } if (LocalDateTime.now().isAfter(task.getEndTime())) { return Result.error(点名已截止); } // 2. 校验是否选课学生 Long studentId CurrentUser.getId(); Integer count courseStudentMapper.selectCount( new LambdaQueryWrapperCourseStudent() .eq(CourseStudent::getCourseId, task.getCourseId()) .eq(CourseStudent::getStudentId, studentId) ); if (count null || count 0) { return Result.error(您未选修该课程); } // 3. 校验WIFI数据合法性 if (req.getWifiSignals() null || req.getWifiSignals().size() 3) { return Result.error(WIFI信号数据不足); } // 过滤掉明显异常数据RSSI不在合理范围 req.getWifiSignals().removeIf(s - s.getRssi() -20 || s.getRssi() -100); // 4. 加载教室指纹库执行匹配 ListWifiSignal fingerprint wifiFingerprintMapper.selectList( new LambdaQueryWrapperWifiSignal() .eq(WifiSignal::getClassroomId, task.getClassroomId()) ); MatchResult match wifiMatchService.matchWifiFingerprint(req.getWifiSignals(), fingerprint); // 5. 插入签到记录唯一索引防重 SignRecord record new SignRecord(); record.setTaskId(task.getId()); record.setStudentId(studentId); record.setCourseId(task.getCourseId()); record.setSignStatus(match.isMatched() ? 1 : 3); record.setWifiReportJson(JSON.toJSONString(req.getWifiSignals())); record.setMatchResult(match.isMatched() ? 1 : 0); record.setDeviceId(req.getDeviceId()); record.setSignTime(LocalDateTime.now()); try { signRecordMapper.insert(record); } catch (DuplicateKeyException e) { return Result.error(请勿重复提交); } // 6. WebSocket推送签到事件到教师端 wsPushService.pushSignEvent(task.getId(), new SignEvent(studentId, match.isMatched(), LocalDateTime.now())); return Result.success(new SignResultVO(match.isMatched(), match.getReason())); }5.3 防代签的多道防线光靠一个WIFI匹配还不够我实际加了几道防线每一道都针对一个真实的作弊场景防线1短时间内重复点名号码共用。后台记录设备指纹deviceId同一台设备如果在多个不同账号上报且时间间隔很短会标记为疑似代签。学生正常情况下一台手机只绑定一个人这里用deviceId studentId形成组合特征。防线2点名窗口随机缩短。教师端可以设置1~2分钟的超短点名窗口大幅压缩帮别人代签的时间窗口。系统默认5分钟实际操作中教师经常改成3分钟。防线3信号静止检测。如果一台设备连续多次上报RSSI值几乎完全不变差值小于2dBm很可能是录屏模拟数据而非真实环境采样。这部分我加了基础校验但不是强判定因为手机放桌上确实可能信号稳定。防线4补签审批流。学生如果因为手机没电等特殊情况没签上可以发起补签申请由老师审批。这个功能看似简单但整个系统就完整了——缺勤不再是无路可走的判罚而是有申诉渠道的流程。5.4 考勤统计与Excel导出统计模块用的是一个联表查询按课程维度计算应到、实到、迟到、缺勤数量public AttendanceStatisticsVO getCourseStatistics(Long courseId) { // 应到人数 选课人数 Long total courseStudentMapper.selectCount( new LambdaQueryWrapperCourseStudent() .eq(CourseStudent::getCourseId, courseId)); // 实到需要在某次点名中签到的去重学生数 // 这里注意一门课可能多次点名取最近一次任务做统计 SignTask latestTask signTaskMapper.selectOne( new LambdaQueryWrapperSignTask() .eq(SignTask::getCourseId, courseId) .orderByDesc(SignTask::getStartTime) .last(limit 1) ); Long present 0L; Long absent 0L; if (latestTask ! null) { present signRecordMapper.selectCount( new LambdaQueryWrapperSignRecord() .eq(SignRecord::getTaskId, latestTask.getId()) .eq(SignRecord::getSignStatus, 1)); absent total - present; } return new AttendanceStatisticsVO(total, present, absent, null); }Excel导出用的EasyExcel字段包括学号、姓名、班级、签到状态、签到时间、设备指纹、WIFI匹配结果。这里我特意把原始WIFI上报数据放到导出的最后一列给老师做异常审计用——如果有人连续几周在同一教室签到但RSSI特征始终完全一致就很可疑。6. Vue 3前端实现教师实时大屏与学生上报页6.1 教师端点名控制台界面布局分三个区域顶部课程信息与操作按钮、中间实时统计卡片、底部学生签到流水列表。核心功能是WebSocket实时推送学生一签到大屏立刻刷新。WebSocket在Vue端的封装// src/utils/ws.js let ws null export function connectSignWs(taskId, onMessage, onError) { const token localStorage.getItem(token) const protocol location.protocol https: ? wss : ws const url ${protocol}://${location.host}/ws/sign/push/${taskId}?token${token} ws new WebSocket(url) ws.onmessage (event) { const data JSON.parse(event.data) onMessage(data) } ws.onerror (e) { onError onError(e) } } export function closeSignWs() { if (ws) { ws.close() ws null } }教师端页面用setInterval每5秒拉一次统计接口作为兜底避免WebSocket断连后数据停滞。6.2 学生端上报页面学生端页面核心逻辑就是读取WIFI列表 组装数据 上报。由于浏览器安全限制直接读取本机WIFI列表的通用方案在Web端并不可行W3C的WIFI API从未被主流浏览器实现过所以我的做法是分两种场景场景A学生使用的是系统配套的移动端H5页面且在同一局域网内前端把当前设备的网络信息连接的SSID、信号强度通过浏览器提供的navigator.connection等接口做辅助。场景B更实际的做法是在学生页面引导用户授权后由后端返回一个本教室应扫描到的目标AP列表前端提示学生连接教室指定WIFI热点如TECH-EDU-5G然后上报连接状态。此时后端根据学生连接了哪个AP Web端能否访问局域网内点名API来辅助判断位置。这个设计要实际说清楚真正完整的获取周围WIFI列表能力在原生App或微信小程序里更可行。毕设项目如果要做到网页端一键采集所有周围WIFI需要做一个小型原生壳或者使用微信小程序的wx.getWifiList接口。如果你做的是纯Web版本建议在论文里明确写本系统采用Web端辅助探测 连接目标AP校验的方式并把原生App采集作为扩展方案这样技术边界清晰答辩也不会被问倒。学生签到页的大致流程async function handleSign() { const taskId route.query.taskId // 1. 检查是否连接到教室热点或与局域网点名API连通 const apConnected await checkApConnection() if (!apConnected) { ElMessage.warning(请先连接教室WIFI热点) return } // 2. 调用后端进行一次快速连通性探测确认可以从当前网络访问到后端API const probe await axios.post(/api/sign/probe, { taskId }) if (probe.data.code ! 200) { ElMessage.error(当前网络无法访问点名服务器) return } // 3. 上报设备信息和网络信息 const deviceId getOrCreateDeviceId() // 本地生成并存储设备唯一标识 const networkInfo await getNetworkInfo() // 获取连接状态、信号强度等 const res await axios.post(/api/sign/report, { taskId, deviceId, networkInfo }) // 4. 展示结果 if (res.data.data.matched) { ElMessage.success(签到成功) } else { ElMessage.error(签到失败未检测到教室信号) } }这里给一个非常有用的经验学生在宿舍里即使连接了教室同名WIFI桥接器转发信号强度和延迟也不会和真实教室完全一致。后端把连通性探测的响应时间控制在5ms以内超过则视为远程连接辅助判定为不匹配。这个细节写进论文里是加分项。6.3 跨域与联调坑前端开发服务器localhost:5173访问后端http://localhost:8080跨域配置在后端做一次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不要写成allowedOrigins(*)否则和allowCredentials(true)冲突SpringBoot3里会直接报错。这个坑我当时的排查过程是前端报CORS错误看了后端日志没有任何请求记录说明请求在Filter层就被拦了把allowedOrigins改成allowedOriginPatterns就好了。7. 实操踩坑记录随机MAC、信号波动与并发点名7.1 设备BSSID采集的随机化问题这是我最初设计方案时最大的坑。Android 10和iOS 14之后系统默认对新的WIFI网络使用随机MAC地址这导致你拿到的设备MAC可能是假的。虽然我们在Web端方案里主要依赖连接AP后端探测但如果扩展原生App采集BSSID和SSID仍是定位的核心数据设备MAC则只做辅助标识。如果要做原生采集注意Android需要动态申请ACCESS_FINE_LOCATION权限Android 13以上还用NEARBY_WIFI_DEVICES替代了部分定位权限用户拒绝授权就无法扫描iOS对CNCopyCurrentNetworkInfo有严格限制非企业签名的应用在iOS 13以后只能获取自己连接的WIFI信息无法扫描周围所有WIFI实际应对策略系统以连接目标AP 后端探测距离为强判定依据以扫描信号列表为辅助数据。这样即使拿不到全量WIFI列表核心点名功能仍可用。这点要在说明文档里讲清楚因为老师很可能会问。7.2 多人在线点名时的并发问题一门120人的大课点名开始时大家同时点击上报后端扛不住一波瞬时并发。我做了两个处理数据库层面唯一索引uk_task_student保证同一任务同一学生只能插入一条记录并发插入时只有一个线程成功其他抛出DuplicateKeyException在代码里捕获并返回已签到而不是报错。应用层面上报接口的WIFI匹配算法是纯内存计算不涉及数据库锁所以瓶颈只在插入。MyBatis-Plus的insert在MySQL默认隔离级别下是行级锁对同一学生并发请求是串行的不会有问题。如果你要处理更高并发可以把WIFI上报先写入Redis队列异步批量落库。但毕设项目到这个程度已经够了不用过度设计。7.3 信号波动导致的误判我的判定逻辑里有一个明显的坑不同手机的RSSI采集灵敏度不一样。同一位置小米手机可能报-45dBm苹果手机可能报-52dBm差7dBm是完全正常的。如果阈值设成12dBm两者都在接受范围内但如果教室指纹库是用小米手机采集的苹果手机实测就会经常触发误判。解决方案是指纹库建库时用多台不同品牌的手机分别采样取一个区间而不是一个单值。具体做法是每台手机采样3次记录该教室里每种AP的RSSI最小值和最大值判定时用区间重叠度替代单点距离如果上报的RSSI落在指纹库区间内记为1分 如果偏离区间但在5dBm内记为0.5分 偏离超过10dBm记为0分。 最终得分 总得分 / 有效AP数 0.7 则匹配这个方案在实测中误判率明显降低尤其解决了苹果手机普遍偏弱的问题。7.4 点名时间窗口的设置点名时长设置太短会被骂太长则给代签留时间。我实测下来班级人数建议时长说明30人以下2分钟小班动作快30~60人3分钟常见大学课堂规模60~120人5分钟大课需要缓冲同时设置一个动态延长策略倒计时结束后如果有学生已进入上报页但未成功提交可以点击延长3分钟但每位老师每堂课最多延长一次。这个功能在教师端就是一个按钮一个计数校验但实际使用中老师们反馈很好。8. 毕设文档与答辩准备从代码到能过关的项目8.1 说明文档和论文要写哪些核心内容标题里的说明文档LW指的就是毕业设计说明书和论文。不要以为代码跑通就大功告成文档写不好同样会被评为工作量不饱满。我建议论文至少包含以下部分需求分析从传统点名痛点出发梳理功能性需求点名、统计、管理和非功能性需求并发、响应时间、安全性WIFI定位原理分析RSSI对数路径损耗模型、指纹匹配算法、阈值设计依据。这是整个项目的技术亮点要重点展开系统设计架构图用Visio或ProcessOn画导出图片嵌到论文里不要用Mermaid画图Word排版兼容性和导出效果都不好数据库设计E-R图、表结构说明重点解释为什么sign_record需要唯一联合索引系统实现每个核心模块贴一段关键代码运行截图代码别贴太长每段控制在30行以内测试功能测试用例表、接口压测用JMeter模拟100并发、WIFI匹配准确率统计8.2 演示Demo的脚本设计答辩演示只有5~10分钟最容易翻车的环节就是现场网络不稳定。我的建议是提前准备一个保底演示方案电脑连接实验室WIFI热点手机也连同一个热点确保局域网互通提前在数据库中造好数据一门课、20个学生、一个教室指纹库演示时先展示后台管理页的指纹库数据证明WIFI指纹不是假的手机连上热点后在浏览器打开学生端页面提交签到成功切换手机到4G/5G网络再提交一次弹出网络不匹配或超时提示证明防代签有效教师端看到实时签到成功记录导出Excel这里有一个小技巧演示WIFI判定时故意切换到手机流量让前端页面加载正常但后端探测失败展示系统的防作弊能力这个反例比正例更有说服力老师通常会当场点头。8.3 答辩高频问题预案以下是答辩时被问到概率最高的问题我把自己准备的回答思路写出来问题1WIFI定位能保证学生一定在教室吗不能百分百保证但系统把判定门槛放在连接目标AP 后端局域网探测 信号区间匹配三重校验上远程/宿舍代签的路径基本被堵死。如果有人拿着学生的手机到教室签到这属于物理手段绕过任何软件签到都防不住需要配合摄像头或人脸识别才能解决这是后续扩展方向。问题2如果教室WIFI信号不稳定怎么办系统设计了三层容错信号区间匹配替代单点匹配、允许学生重试并取平均、教师可手动发起补签审批。此外指纹库支持动态校准任课老师现场采样后可一键更新。问题3为什么不用蓝牙或iBeacon蓝牙需要额外的基站设备或者让所有学生开启蓝牙部署成本更高且蓝牙信号的穿透性和稳定性在教室场景并不优于WIFI。WIFI是教室已有设施零额外硬件成本。问题4报表导出用的是什么技术EasyExcel基于Apache POI封装支持百万级数据量导出还支持模板填充。问题5JWT的Token过期怎么处理前端在请求拦截器里判断后端返回的401状态码自动跳转登录页重新登录。业务中设置了2小时有效期一次课堂时长内基本不需要重新登录。9. 我实际做完这个项目的一些体会整个系统从设计到跑通连带写论文前后大概用了三周。回头复盘最花时间的不是CRUD代码而是两件事WIFI判定阈值调的准确率以及前后端联调时的WebSocket鉴权问题。阈值调试需要反复在真实教室环境里采样、测试、调整数据量大了才有效果。WebSocket的握手鉴权比普通REST接口麻烦JWT不能通过URL参数安全传递会被日志记录我最后是用Header传token在HandshakeInterceptor里做校验。这个细节你们做的时候大概率也会遇到提前有个心理准备。如果你是第一次做完整的前后端分离项目我的建议是先别急着写代码花两天时间把数据库表结构和核心接口定义好再动手实现。WIFI指纹匹配算法可以先写一个独立的Java类用main函数跑通再集成到SpringBoot里这样调试速度快很多。做一个有真实场景、有技术深度的毕设项目收获的不仅是一篇能过审的论文更是你自己能在面试时讲清楚我设计了一个什么样的系统、遇到过什么问题、怎么解决的底气。这套基于WIFI协议的课堂点名系统希望也能成为你的敲门砖。
返回列表