ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Java+uniapp双定位考勤系统:WiFi指纹与GPS围栏落地实战

Java+uniapp双定位考勤系统:WiFi指纹与GPS围栏落地实战 简介本资源是一套基于Java与安卓UniApp的WiFi和GPS双定位学生课程考勤管理系统毕业设计源码面向计算机、软件工程、通信工程等专业的在校学生与教师可用于毕业设计、课程设计、作业或项目立项演示。项目采用SSM后端与UniApp跨端前端通过WiFi与GPS双重定位实现课堂考勤签到解决传统点名效率低、代签难防的问题。压缩包共455个文件以270个js与112个vue为核心业务代码辅以scss、jsx、css等样式与组件文件另有sql数据库脚本、md使用文档及少量图片与配置文件整体约1019KB结构清晰便于二次开发。已有98人学习关注作者为qq_21531681。该资源已通过Mac与Windows10/11运行测试答辩评审95分读者可获取完整源码、数据库与使用文档快速理解双定位考勤的实现思路并在此基础上修改扩展功能适合作为高分毕设参考与进阶学习素材。1. 双定位考勤系统从 WiFi 指纹到 GPS 围栏的完整落地学生考勤这件事看起来只是「点个到」真做起来才知道坑有多深。纯 GPS 在室内直接飘到几百米外纯 WiFi 又容易被热点名伪造所以这套基于 Java 安卓 uniapp 的课程考勤管理系统走的是 WiFi 指纹 GPS 地理围栏双通道校验的路子服务端用 SSM 记录每个教室的 BSSID 白名单和经纬度半径客户端在 uniapp 里同时采集两类信号任一通道命中且时间窗口合法才判定有效签到。它解决的是「代签、异地签、室内定位不准」这三个毕业设计里最常被导师追问的痛点适合做课设、毕设也适合想学移动端定位落地的同学拿来拆。整套资源包含后端源码、安卓端、数据库脚本和使用文档下面按「是什么 → 怎么跑 → 坑在哪」的顺序拆开讲。2. 双定位校验的技术选型为什么不是单 GPS 也不是单 WiFi2.1 WiFi 指纹与 GPS 围栏各自的边界先说清楚为什么非得两个一起上。GPS 的强项是室外开阔环境民用模块水平误差常见在 515 米但一进教学楼钢筋混凝土一挡误差能飙到 50 米以上甚至直接返回上一次的缓存坐标这就是热词里说的「gps误差」。WiFi 定位靠的是 BSSID路由器 MAC 地址唯一性室内精度反而好一个教室对应几个固定 AP扫到的 BSSID 集合匹配上就算命中但它怕两件事一是热点名可以伪造二是安卓从 6.0 起对 WiFi 扫描加了频率限制和定位权限绑定。所以这套系统的判定逻辑是「或」关系而非「与」GPS 在半径内或者 WiFi 指纹匹配成功任一成立即通过。这样室内靠 WiFi 兜底室外靠 GPS 兜底单点失效不会让整个签到瘫掉。选型上后端用 SSMSpring SpringMVC MyBatis而不是 SpringBoot是因为毕设答辩时导师更认传统三层架构配置显式、分层清晰改起来也好定位。2.2 数据库表结构与定位字段设计定位相关的核心表就三张教室表存基准坐标和半径AP 表存 BSSID 白名单签到记录表存每次采集的原始信号。字段设计直接决定后面判定好不好写。-- 教室表一个教室一个地理围栏 CREATE TABLE classroom ( id INT PRIMARY KEY AUTO_INCREMENT, room_name VARCHAR(50) NOT NULL, latitude DECIMAL(10,7) NOT NULL, -- 基准纬度7位小数约1cm精度 longitude DECIMAL(10,7) NOT NULL, -- 基准经度 radius INT DEFAULT 80, -- 允许半径(米)室内建议50-100 status TINYINT DEFAULT 1 ); -- AP白名单表一个教室可挂多个路由器 CREATE TABLE ap_whitelist ( id INT PRIMARY KEY AUTO_INCREMENT, classroom_id INT NOT NULL, bssid VARCHAR(17) NOT NULL, -- 格式 AA:BB:CC:DD:EE:FF ssid VARCHAR(64), FOREIGN KEY (classroom_id) REFERENCES classroom(id) ); -- 签到记录表保留原始信号便于申诉复核 CREATE TABLE attendance_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, check_time DATETIME NOT NULL, latitude DECIMAL(10,7), longitude DECIMAL(10,7), matched_bssid VARCHAR(17), locate_type TINYINT, -- 1GPS 2WiFi 3双命中 distance INT, -- 距基准点米数便于排查 status TINYINT DEFAULT 1 );latitude/longitude用DECIMAL(10,7)而不是FLOAT是因为浮点在距离计算里会累积误差7 位小数对应约 1 厘米足够围栏判定。radius默认 80 米是室内场景的经验值太小会误杀站在教室门口的学生太大又失去约束意义。attendance_record特意保留matched_bssid和distance答辩时被问「凭什么判定他在教室」可以直接调记录自证。2.3 后端判定接口的实现判定逻辑集中在签到接口里先算 GPS 距离再查 WiFi 白名单最后落库。// AttendanceController.java 核心签到逻辑 RequestMapping(value /checkIn, method RequestMethod.POST) ResponseBody public MapString, Object checkIn(RequestBody CheckInDTO dto) { MapString, Object result new HashMap(); Classroom room classroomService.getById(dto.getClassroomId()); boolean gpsPass false; boolean wifiPass false; int distance -1; // 1. GPS 围栏判定Haversine 公式算球面距离 if (dto.getLatitude() ! null dto.getLongitude() ! null) { distance GeoUtil.getDistance( room.getLatitude(), room.getLongitude(), dto.getLatitude(), dto.getLongitude()); gpsPass distance room.getRadius(); } // 2. WiFi 指纹判定扫到的 BSSID 是否在白名单 if (dto.getBssidList() ! null !dto.getBssidList().isEmpty()) { ListString white apService.listBssidByRoom(dto.getClassroomId()); for (String bssid : dto.getBssidList()) { if (white.contains(bssid.toUpperCase())) { wifiPass true; dto.setMatchedBssid(bssid); break; } } } // 3. 任一通过即有效记录定位类型 if (gpsPass || wifiPass) { AttendanceRecord rec new AttendanceRecord(); rec.setStudentId(dto.getStudentId()); rec.setCheckTime(new Date()); rec.setDistance(distance); rec.setMatchedBssid(dto.getMatchedBssid()); rec.setLocateType(gpsPass wifiPass ? 3 : (gpsPass ? 1 : 2)); attendanceService.save(rec); result.put(code, 200); result.put(msg, 签到成功); } else { result.put(code, 400); result.put(msg, 不在考勤范围GPS距离 distance 米); } return result; }GeoUtil.getDistance用的是 Haversine 公式比直接算平面欧氏距离准尤其在纬度较高地区。locateType字段区分三种命中方式方便后期统计「到底有多少人靠 WiFi 兜底」。注意bssid.toUpperCase()这一步不能省安卓不同厂商返回的大小写不一致血泪经验不统一大小写会出现「明明连上了却匹配不上」的玄学问题。3. uniapp 安卓端定位采集权限、扫描与上报3.1 安卓定位权限的申请顺序安卓端最容易被卡住的就是权限。从 Android 6.0 起定位是运行时权限10 以后又拆出后台定位12 之后 WiFi 扫描还必须开定位开关否则getScanResults直接返回空列表。正确顺序是先申请ACCESS_FINE_LOCATION再判断系统定位总开关是否打开最后才发起扫描。// uniapp 中申请定位权限并检查开关 async function ensureLocationReady() { // 1. 申请精确定位权限 const auth await new Promise((resolve) { plus.android.requestPermissions( [android.permission.ACCESS_FINE_LOCATION, android.permission.ACCESS_COARSE_LOCATION], (e) resolve(e.granted), (e) resolve([]) ); }); if (!auth || auth.length 0) { uni.showToast({ title: 请授予定位权限, icon: none }); return false; } // 2. 检查系统定位总开关Android 12 必须 const main plus.android.runtimeMainActivity(); const context main.getApplicationContext(); const locationManager context.getSystemService(location); const enabled locationManager.isLocationEnabled(); if (!enabled) { uni.showToast({ title: 请打开手机定位开关, icon: none }); return false; } return true; }requestPermissions的回调里granted是已授权数组长度为零说明被拒。第二步的isLocationEnabled是 Android 12 之后判断定位总开关的推荐方式比读Settings.Secure.LOCATION_MODE更稳。很多同学只做了第一步结果在 Android 12 手机上扫描永远为空翻车就翻在这。3.2 WiFi 扫描与 GPS 坐标采集权限过了之后WiFi 扫描和 GPS 采集可以并行发起减少等待时间。// 采集 WiFi 列表 GPS 坐标统一上报 async function collectAndReport(courseId, classroomId) { const payload { courseId, classroomId, bssidList: [] }; // WiFi 扫描注意 Android 9 有节流两次扫描间隔需 30s const wifiList await new Promise((resolve) { const main plus.android.runtimeMainActivity(); const ctx main.getApplicationContext(); const wifiManager ctx.getSystemService(wifi); const list wifiManager.getScanResults(); const arr []; for (let i 0; i list.size(); i) { const item list.get(i); arr.push(item.BSSID); // 只取 MAC不取 SSID 避免中文乱码 } resolve(arr); }); payload.bssidList wifiList; // GPS 坐标用 uni.getLocationtype 选 gcj02 与后端坐标系一致 const loc await new Promise((resolve, reject) { uni.getLocation({ type: gcj02, success: resolve, fail: reject }); }); payload.latitude loc.latitude; payload.longitude loc.longitude; // 上报后端 return uni.request({ url: BASE_URL /attendance/checkIn, method: POST, data: payload }); }getScanResults返回的是最近一次系统扫描的缓存不是实时扫描所以如果刚开机可能为空常见做法是先调startScan()再延迟 12 秒取结果。type: gcj02必须和后端存的坐标系一致如果后端存的是 WGS84 而前端传 gcj02会有几十到上百米的系统性偏移这也是「gps误差」里最隐蔽的一种。只取 BSSID 不取 SSID是因为部分安卓机 SSID 含中文会乱码而判定根本用不到 SSID。3.3 上报数据的字段对照前端 payload 和后端 DTO 字段必须一一对应错一个就 400。对照关系如下前端字段后端 DTO 字段类型说明courseIdcourseIdInteger课程 IDclassroomIdclassroomIdInteger教室 IDbssidListbssidListListString扫描到的 MAC 列表latitudelatitudeDoublegcj02 纬度longitudelongitudeDoublegcj02 经度字段名大小写敏感bssidList写成bssidlist后端接不到这种低级错误在联调时最耗时间建议前后端字段表先对齐再写代码。4. 避坑与排查双定位系统最常见的五个翻车点4.1 现象室内 GPS 飘到几百米外签到被判无效原因安卓在室内拿不到卫星信号时会返回上一次定位缓存或基站粗定位精度字段accuracy可能高达几百米。解决在uni.getLocation的 success 回调里读loc.accuracy大于 100 米直接丢弃 GPS 结果只走 WiFi 判定别让脏数据污染围栏计算。4.2 现象WiFi 扫描列表为空明明连着路由器原因Android 10 以后getScanResults需要定位权限且系统定位开关打开缺一不可另外部分机型两次扫描间隔小于 30 秒会返回空。解决先跑 3.1 的ensureLocationReady再在扫描前调startScan()并延迟取结果别直接读缓存。4.3 现象BSSID 匹配不上白名单里明明有原因安卓返回的 BSSID 大小写不统一有的机型返回小写数据库存的是大写。解决后端比对前统一toUpperCase()前端上报时也统一转大写两头都做别只做一头。4.4 现象GPS 坐标对不上整体偏移几十米原因前端用 gcj02后端存 WGS84坐标系不一致。解决全链路统一坐标系要么都用 gcj02要么后端入库前做一次转换别混用。这个坑最隐蔽因为偏移量不大容易误判成「GPS 精度问题」。4.5 现象签到接口 400后端收不到 bssidList原因uniapp 的uni.request默认content-type是application/json但如果后端用RequestParam接数组格式对不上。解决后端用RequestBody接 DTO前端保持 JSON 提交两边对齐。联调时先看浏览器或抓包工具的原始请求体比猜快得多。5. 进阶技巧把定位判定做成可配置的评分制前面讲的「或」判定简单直接但答辩时容易被追问「万一有人站在两个教室中间呢」。更稳的做法是把硬判定改成评分制GPS 距离越近分越高WiFi 命中数越多分越高总分过阈值才算有效。这样既能处理边界情况也显得系统有设计深度。// 评分制判定GPS 距离分 WiFi 命中分 public int calcScore(Classroom room, CheckInDTO dto) { int score 0; // GPS 分半径内满分 60超出按距离衰减 if (dto.getLatitude() ! null) { int dist GeoUtil.getDistance(room.getLatitude(), room.getLongitude(), dto.getLatitude(), dto.getLongitude()); if (dist room.getRadius()) { score 60; } else if (dist room.getRadius() * 2) { score 60 - (dist - room.getRadius()) / 2; // 每超1米扣0.5分 } } // WiFi 分每命中一个白名单 AP 加 20上限 40 if (dto.getBssidList() ! null) { ListString white apService.listBssidByRoom(room.getId()); int hit 0; for (String b : dto.getBssidList()) { if (white.contains(b.toUpperCase())) hit; } score Math.min(hit * 20, 40); } return score; }阈值建议设在 60 分单靠 GPS 命中就是 60 分刚好过单靠 WiFi 命中两个 AP 是 40 分不够、三个才 60 分逼着室内场景多扫几个 AP反而更可靠。这套评分逻辑改起来只动一个方法不影响表结构答辩演示时还能现场调阈值展示灵活性。验证方法很简单拿两台手机一台站在教室中心一台站在走廊尽头分别签到看分数差异再把其中一台的 GPS 关掉只留 WiFi确认兜底通道生效。我一般会在attendance_record里加一列score存下每次得分事后统计分布如果大量记录卡在 5565 分之间说明阈值或半径需要调。从那以后我每次做定位类项目都强制先把坐标系和权限顺序在真机上跑通再写业务逻辑不然联调阶段全是玄学问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表