
简介这是一套基于Java与安卓uniapp开发的WiFi和GPS双定位学生课程考勤管理系统面向计算机相关专业学生可用于毕业设计、课程设计、作业演示或项目初期立项。系统将WiFi指纹与GPS定位结合进行考勤签到能有效减少代签和位置不准确的问题也可实时查看考勤记录与统计报表。整套资源共455个文件压缩包约1019KB前端以270个JavaScript、112个Vue组件、15个SCSS样式为主覆盖页面交互与界面布局后端基于Java SSM框架提供稳定接口并另含2个SQL数据库脚本及Markdown、TXT等多格式使用文档目录结构清晰便于按模块逐个拆解学习。代码已在macOS及Windows 10/11上运行通过答辩评审分为95分整体稳定性好可作为高分毕设参考。目前已有98人学习下载适合具备一定基础的学生在此基础上扩展考勤统计、课程管理等功能也可直接用于毕业设计、课程设计等场景。1. 学生考勤管理系统为什么要同时用 WiFi 和 GPS双定位不是卖点是刚需一个教室一百多人点名十分钟老师一节课的黄金时间全耗在“张三到了吗”上面——这大概是考勤管理系统存在的最朴素理由。但你要是只用 GPS 做定位考勤翻车速度比你想象中快得多手机定位漂到隔壁楼、室内根本没有卫星信号、还有人开着位置模拟器直接签到。反过来只依赖 WiFi又会踩教学楼多 AP 覆盖的坑信号强度和实际位置并不线性对应。这套基于 Java 安卓 UniApp 的 WiFi 和 GPS 双定位学生课程考勤管理系统核心就是让两种定位源互相兜底GPS 负责室外大范围锁定WiFi 负责室内楼栋级判定服务端再做一次融合。适合正在做毕业设计、想拿到一套能答辩的 Java Web 移动端方案的在校生也适合想在校内小范围试点考勤信息化、又不想上商业 SaaS 的老师。2. 系统架构与冷启动先拆 Java 后端和 UniApp 前端的边界2.1 为什么是两层而不是三层后端职责与前端职责的划分考勤系统的产品形态很固定后台给教师用管理课表、看签到报表移动端给学生用完成定位签到。很多毕业设计会纠结要不要写一个 PC 管理端其实没必要——UniApp 这一套前端代码编译成 App 之后教师端页面完全可以在 H5 里跑或者复用同一套 Vue 页面减少一套开发量。从标题里的关键词就能看出技术边界Java 负责服务端提供 REST 接口安卓 UniApp 负责移动端展示和定位采集。这里有一个容易被答辩老师问住的设计决定为什么双定位判定的逻辑要放在服务端而不是前端算好结果直接提交因为前端任何计算结果都可以被篡改一个懂点逆向的学生就能伪造“定位成功”的请求。把 GPS 坐标、WiFi 扫描列表原始数据提交给后端后端根据自己的数据表做距离计算和 AP 匹配再把结论落库这样才算闭环。Java 后端这块常见做法是 Spring Boot MyBatis 的组合。Spring Boot 胜在配置少、内嵌 Tomcat、能直接打包成可执行 jar演示的时候一条java -jar命令就能启动。MyBatis 则适合写统计报表类 SQL考勤统计里大量 GROUP BY 聚合查询用 MyBatis 的 XML 或注解写都比 JPA 那种自动生成的查询更直观。数据库通常选 MySQL 5.7 或 8.0标题里的“数据库”说的就是初始化 SQL 脚本和表结构。2.2 manifest.json 是 UniApp 考勤前端的成败关键拿到 UniApp 项目之后第一个要打开的不是代码而是根目录下的manifest.json。这个文件决定了 App 在打包和真机运行时的基本行为。很多同学在这上面耗掉一整天uni.getLocation()在 HBuilderX 内置浏览器里跑得好好的一换到真机调试就调 fail 回调十有八九就是 manifest 配置漏了。用 HBuilderX 打开项目后进入“重新生成 appid”、App 模块配置里勾选定位GPS 定位和地图模块再把 App 权限配置里的定位权限全部勾上。工具类项目还要注意一个细节权限列表里有一项“获取位置信息”它依赖 Android 系统层的位置权限必须同时存在于AndroidManifest.xml里。UniApp 的云端打包会在 manifest.json 配置的基础上自动生成清单文件所以只要保证 HBuilderX 的可视化配置里勾选齐全即可。关于 manifest 配置容易忽略的点我用下表说明配置位置用途典型值 / 操作基础配置 → AppID区分应用真机运行需要点击重新获取模块配置 → 定位启用系统定位能力勾选 GPS 定位权限配置 → 位置信息申请 ACCESS_FINE_LOCATION全部勾选源码视图 → app-plus.distribute.sdkConfigs集成第三方定位 SDK按实际需要配置 key这里多说一句边界如果把 UniApp 编译成微信小程序GPS 定位还能拿到但 WiFi 扫描结果基本拿不到双定位就退化成单定位了。所以这套系统要完整发挥能力必须打包成安卓 App而不是只跑小程序。2.3 本地跑通最小链路从 SQL 导入到接口联调拿到源码压缩包后建议按“数据库 → 后端 → 前端”的顺序冷启动。先把数据库建好再启动后端验证接口能否通最后才用 HBuilderX 跑前端。一次顺序搞反后面排查起来会同时面对前端报错和后端报错很难定位。# 第一步导入数据库脚本 mysql -uroot -p sql/attendance.sql # 第二步修改后端数据库连接配置 vim src/main/resources/application.yml # 第三步启动后端开发环境 mvn spring-boot:run # 第四步确认后端健康检查接口返回 JSON curl http://localhost:8080/api/health这里每个步骤都有讲究。第一步里的attendance.sql是完整初始化脚本包含建库、建表、初始管理员账号和一批测试学生数据导入成功意味着后面接口联调时有数据可用。第二步里application.yml要改的是spring.datasource.url、username、password三项URL 里注意 MySQL 8.0 和 5.7 的驱动差异8.0 需要在 URL 上追加useSSLfalseserverTimezoneAsia/Shanghai否则启动直接报时区错误。第四步的/api/health是后端预留的健康检查接口虽然不参与业务但能快速判断服务是否起来。没有这个接口的话项目结构里第一个能 GET 访问的 Controller 同样可以替代。至于前端HBuilderX 导入后需要在config.js这类公共配置里把baseURL改成http://局域网IP:8080注意真机不能用localhost必须写电脑的局域网 IP并且手机和电脑在同一 WiFi 下。2.4 拿到源码先读哪几个文件前 30 分钟阅读顺序源码不要从第一个文件顺序翻没意义。按下面的顺序读三十分钟内就能建立完整认知文件读它解决什么问题sql/attendance.sql搞清表结构、状态字段取值范围application.yml搞清端口、数据库、日志级别后端AttendanceController搞清签到相关的全部接口路径和入参前端pages/checkin/checkin.vue搞清定位采集和提交的完整流程attendance.sql是信息量最大的单文件。它里面的status字段如果定义成0/1/2那 0 是正常、1 是迟到还是缺勤每个项目定义不一样先看注释或枚举类避免后面写代码时把状态值搞反。application.yml里除了数据库连接server.port决定接口端口logging.level控制 SQL 日志打印排查定位问题时需要把logging.level.com.example.mapperdebug打开才能看到 MyBatis 执行的实际 SQL。签到 Controller 决定了前端怎么调接口。先看方法上的PostMapping、GetMapping路径再看参数类里的字段名最后确认返回值结构。返回结构统一用Result包装类code、message、data是这类系统最常见的约定前端所有请求都按这个结构解包。我把这套流程里最关键的“后悔药”建议放在 2.3修改任何配置之前先给application.yml和manifest.json各做一个副本。原因很简单这两个文件一旦改坏项目直接起不来或打不出包而毕业设计期间你往往不知道最初的配置是什么了。3. 双定位核心GPS 与 WiFi 的采集、融合判定与兜底3.1 前端 GPS 采集uni.getLocation 的坐标系与精度取舍双定位系统的第一路信号来自 GPS。UniApp 提供的uni.getLocation封装了系统定位能力在安卓 App 端走的是原生定位服务。代码层面签到页面里的定位逻辑是这样的// pages/checkin/checkin.vue uni.getLocation({ type: gcj02, accuracy: high, success: (res) { console.log(GPS定位结果, res.latitude, res.longitude, res.accuracy); // 把 GPS 结果和后续要采的 WiFi 指纹一起组合提交 this.submitLocation(res.latitude, res.longitude, res.accuracy, this.wifiList); }, fail: (err) { // GPS 拿不到定位室内或信号遮挡记录错误并尝试 WiFi 兜底 console.log(GPS定位失败, err); this.wifiOnlyFallback(err); } });这段代码有两个关键参数。type: gcj02表示返回的是国测局加密坐标适合直接画在高德底图上如果项目用的天地图或 Google 地图才需要考虑wgs84类型。多数考勤系统不需要展示地图只是把坐标提交给后端算距离这时用gcj02和wgs84的区别在于标准 GPS 设备上报的是wgs84如果你做精度测试时拿手机 App 和手持 GPS 对比二者会差几十米这不是定位误差是坐标系导致的偏移。accuracy: high是 UniApp 对定位精度的建议值。有的安卓机型在“省电模式”下会把 GPS 定位降级成基站定位精度从 5 米劣化到 500 米。代码里还要处理res.accuracy字段当它大于某个阈值时这次定位结果不可信。我一般把可信阈值设在 50 米以内超过就直接走 WiFi 判定不要硬算距离。3.2 WiFi 指纹采集安卓原生模块把扫描结果回传WiFi 这一路信号在 UniApp 里稍微有点特殊JS 层没有直接暴露扫描周围热点列表的 API必须在插件市场找现成的原生插件或者用 Android Studio 写一个小原生模块把结果通过事件或回调传回 Vue 页面。对于毕业设计我建议直接用插件市场里维护频率高的 WiFi 插件省掉自己封装原生模块的调试时间。先看一个原生模块的典型实现逻辑这能帮你判断插件返回的数据长什么样// 原生模块中的 WiFi 扫描逻辑伪代码 WifiManager wifiManager (WifiManager) context.getSystemService(Context.WIFI_SERVICE); ListScanResult results wifiManager.getScanResults(); JSONArray array new JSONArray(); for (ScanResult result : results) { JSONObject item new JSONObject(); item.put(ssid, result.SSID); item.put(bssid, result.BSSID); item.put(rssi, result.level); // 单位是 dBm负数 array.put(item); }这段逻辑说明三个事情。第一扫描结果里rssi是负数数值越大越接近 0说明信号越强这是后续算评分的基础。第二iOS 和安卓系统对 WiFi 扫描的隐私限制不同安卓 8.0 之后定位权限与 WiFi 扫描是绑定的没有位置权限连扫描列表都是空的所以前端在请求定位权限时顺便把 WiFi 扫描必需的权限也拿下来。第三getScanResults()返回结果有缓存两次调用之间至少需要几秒间隔否则拿到的还是上一次的旧数据。前端拿到扫描列表后把它和 GPS 坐标一起放进一个请求体里提交。请求结构形如{ courseId: 1001, studentId: 2023001, latitude: 30.123456, longitude: 120.123456, accuracy: 15, wifiList: [ {ssid: Teaching-Building-3F, bssid: aa:bb:cc:dd:ee:ff, rssi: -48}, {ssid: Canteen-Free-WiFi, bssid: 11:22:33:44:55:66, rssi: -65} ] }服务端拿到wifiList后才有资格做融合判定。请不要只提交 GPS 值。那样做的话当学生在教学楼内 GPS 失效时后端无从判断到底该信哪一路信号。3.3 服务端融合判定一段可跑的 Java 代码把两路信号合成一个结论服务端需要两张基础数据才能做判定一张存每个教室的经纬度坐标一张存教室附近 AP 的 BSSID 与信号基准值。前者用于 GPS 距离计算后者用于 WiFi 指纹评分。GPS 距离计算用 Haversine 公式就够了精度对几十米量级不是问题。WiFi 评分可以用一个很实用的模型把扫描到的每个 BSSID 和数据库里该教室的 AP 表对比如果在表里就把数据库中的基准 RSSI 与当前扫描 RSSI 的差值取绝对值差值越小说明信号强度和上课时一致再叠加上“能扫到多少个教室内部 AP”这一项做加权评分。public boolean checkLocation(CheckinRequest req, Course course) { // 1. GPS 判定实际坐标与教室坐标的距离 double distance haversine( req.getLatitude(), req.getLongitude(), course.getLat(), course.getLng()); boolean gpsPassed distance GPSScoreThreshold.DISTANCE_M; // 2. WiFi 判定扫描结果与教室 AP 基准比较 int signalScore 0; int matchedApCount 0; for (WifiItem item : req.getWifiList()) { Integer baseline classroomApMapper.findBaseline(course.getId(), item.getBssid()); if (baseline ! null) { signalScore Math.abs(baseline - item.getRssi()); matchedApCount; } } // 至少 2 个匹配 AP且平均偏差小于 12 dBm认为 WiFi 条件成立 boolean wifiPassed matchedApCount 2 signalScore / matchedApCount WifiScoreThreshold.DIFF_DBM; // 3. 融合判定的核心策略GPS 过就算过GPS 不过但 WiFi 过算过 // 反过来 WiFi 不过GPS 过也过。只有两路都不过才拒绝。 return gpsPassed || wifiPassed; }这里参数怎么定是答辩时能拉开差距的地方。distance 50米适用于教室级考勤超过 50 米容易把隔壁楼的学生也算进来。WiFi 侧的DIFF_DBM 12来自实际经验人在同一个座位时RSSI 波动范围在正负 5 dBm在隔壁教室可能差 15 dBm 以上。但要根据教学楼结构调整墙体厚薄差异很大。还有一个常被忽略的参数matchedApCount阈值为什么是 2。如果一个教室只有一台 AP靠它判定会经常误伤因为教室里人数多、AP 并发占用高、信号波动大两台以上同时命中概率上靠谱得多。系统里这套逻辑被封装在独立的LocationVerifyService里前端只是采集数据服务端才是裁判。这样设计的好处是后续规则调整不需要重新发版 App改后端参数即可。3.4 后台定位、模拟器检测与隐私合规的兜底策略考勤场景的正确定位时机是“点击签到那一刻”而不是 App 在后台时持续定位。很多人一听到后台定位就想到用userlocationbackground这个参数做持续监听这在考勤里属于过度设计既耗电又踩隐私合规的坑。安卓后台长时间扫描 WiFi 会被系统直接掐掉测试时会得到一个“定位失败”的假象。我建议的是签到页面onShow时请求一次定位和一次 WiFi 扫描如果失败给用户一个“信号差请靠近窗边重试”的提示。对考勤场景这就够了。防模拟器是另一个让老师问不倒的点。安卓开发者选项里“选择模拟位置信息应用”可以把 GPS 值改到任意地点很多学生就是用这个绕过考勤。简单的防线是在后端比对连续两次定位前端在点击签到后隔 3 秒自动再采一次位置两次坐标经 Haversine 距离计算若超过 30 米说明第一次和第二次位置不一致服务端判定为可疑签到。原理是正常坐着的学生位置不会在两三秒内移动几十米。隐私方面考勤系统采集的是课堂范围内的位置信息需要有明确的使用目的说明。后端持久化时只记录签到时刻的坐标不要定时批量采集位置轨迹这也符合最小必要原则。4. 考勤业务闭环课表、签到状态机与统计报表4.1 数据库表设计学生、课程、考勤记录三张核心表双定位只是手段考勤业务才是系统真正被使用的部分。数据库里最核心的三张表是学生表、课程表、考勤记录表。设计它们时要把“状态字段”和“定位冗余字段”都提前想好。表名关键字段设计要点studentid,student_no,name,class_name,device_nodevice_no存设备唯一标识用于防重复签到辅助判断courseid,course_name,teacher_id,start_time,end_time,classroom,lat,lng课的经纬度是后续 GPS 判定的基准值attendanceid,student_id,course_id,date,status,checkin_time,latitude,longitude,location_sourcestatus用枚举location_source存 GPS/WiFi/双定位哪个来源通过的考勤表的status字段是业务核心。建议用0正常、1迟到、2缺勤、3请假。注意不要用中文直接存把状态枚举写死在 Java 代码或 MyBatis 的 typeHandler 里统计时读出来映射成中文展示。schema.sql设计表结构时还有一个容易出错的地方attendance表的唯一索引。业务规则明确一个学生上同一门课同一天只能有一条考勤记录所以唯一索引必须建在(student_id, course_id, date)上否则重复点击签到会插入多条记录统计出勤率时全乱。用一段 DDL 把这层约束固化下来CREATE TABLE attendance ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, course_id BIGINT NOT NULL, date DATE NOT NULL, status TINYINT NOT NULL DEFAULT 0, checkin_time DATETIME NULL, latitude DECIMAL(10,6) NULL, longitude DECIMAL(10,6) NULL, location_source VARCHAR(16) NULL, UNIQUE KEY uk_student_course_date (student_id, course_id, date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;经纬度字段用DECIMAL(10,6)不是FLOAT。FLOAT的精度在小数点后 5 位开始丢坐标计算时常产生 10 米级误差这在考勤判定里是致命的。utf8mb4是为了兼容中文学生姓名和特殊字符用老式utf8会出现插入 emoji 昵称报错的问题。4.2 签到接口的状态流转从点击按钮到落库的整个时序签到接口是系统的核心入口。前端点击“签到”开始到数据库插入一条记录中间经过了课程校验、时间窗校验、位置校验、重复签到校验四道关卡。这个时序流程后端 Controller 的一段代码就能看清楚PostMapping(/api/attendance/checkin) public Result checkin(RequestBody CheckinRequest req) { // 1. 校验课程是否存在并在有效签到时间窗内 Course course courseMapper.selectById(req.getCourseId()); Date now new Date(); int beforeStart Config.getInt(checkin.beforeStart, 10); // 课前10分钟 int lateLimit Config.getInt(checkin.lateLimit, 15); // 迟到判定 if (now.before(addMinutes(course.getStartTime(), -beforeStart))) { return Result.err(未到签到时间); } // 2. 位置校验 boolean locationOk locationVerifyService.checkLocation(req, course); if (!locationOk) { return Result.err(当前位置不在教室范围); } // 3. 防重复签到由唯一索引兜底但先查一次给出友好提示 Attendance exist attendanceMapper.selectByStudentCourseDate( req.getStudentId(), req.getCourseId(), new Date()); if (exist ! null) { return Result.err(今日已完成签到); } // 4. 落库 Attendance a new Attendance(); a.setStudentId(req.getStudentId()); a.setCourseId(req.getCourseId()); a.setDate(new Date()); a.setLatitude(req.getLatitude()); a.setLongitude(req.getLongitude()); a.setLocationSource(req.getLocationSource()); // 迟到判断晚于上课 start_time 进入迟到状态 a.setStatus(now.after(course.getStartTime()) ? AttendanceStatus.LATE : AttendanceStatus.NORMAL); attendanceMapper.insert(a); return Result.ok(a); }这段代码里最贴近实际业务的判断是“迟到判定”。课程表里的start_time是固定值但老师实际打卡可能因为拖堂等晚几分钟因此系统往往把迟到阈值做成配置参数放在application.yml里而不是硬编码。服务端做not before“不能早于课前 10 分钟”的判断时要注意边界如果课程 8:00 开始7:50 前点击签到应该被拒绝。可用时间窗内学生点击签到才被接受。毕业生最容易翻车的地方是把课前 10 分钟写死成-10分钟当课程表里的时间是下午学时没影响一旦换成早上 8:00发现 7:50 就可以签这里建议明确用分钟数偏移而不是Date减法。4.3 防代签的实用关卡时间窗随机偏移与连续位置校验代签是考勤系统落到真实使用场景时被质问最多的功能。学生在宿舍把位置模拟到教室或让室友拿自己手机去签前端和后端都无法完全杜绝。但可以把代签成本抬高到不划算。第一道关卡是签到时间窗的随机偏移。老师不想让学生算准时间卡点签到可以给每个课程配置一个windowSeed随机种子服务端用这个种子对课前 10 分钟和课后 5 分钟之间生成一个随机偏移。比如约定“提前多少分钟可以签”每次都不一样学生就没办法提前写好定时任务。第二道关卡是 3.4 提到的连续定位采样。签到时前端连续采集两次位置第二次采集结果单独作为一个接口字段提交。后端比较两次坐标的距离double firstToSecond haversine(req.getLatitude(), req.getLongitude(), req.getSecondLat(), req.getSecondLng()); if (firstToSecond 30) { return Result.err(检测到位置异常变化请重新签到); }学生坐在座位上两次定位间隔 3 秒距离通常小于 10 米。即使 GPS 漂移正常情况下漂移也是围绕真实位置随机分布的不会在 3 秒内稳定移动 30 米。这套方案比“必须连接教室 WiFi”的老式方案顺手因为不需要学生在教室找到特定 WiFi 信号也不存在连上又掉线的摩擦。4.4 统计报表按课程、班级、周次聚合出勤率的 SQL考勤系统的价值终点是统计报表。教师端至少要能看到按课程统计的出勤率和按班级统计的迟到率。这类查询是 MyBatis 最喜欢处理的工作。-- 按课程 班级统计到课率 SELECT c.course_name, s.class_name, COUNT(DISTINCT s.id) AS total_students, COUNT(DISTINCT CASE WHEN a.date CURDATE() THEN a.student_id END) AS present_today FROM course c JOIN student s ON s.class_name c.target_class_name LEFT JOIN attendance a ON a.course_id c.id AND a.student_id s.id AND a.status IN (0, 1) WHERE c.id #{courseId} GROUP BY c.course_name, s.class_name;这个 SQL 的巧妙之处在于COUNT(DISTINCT s.id)算的是该课程应到人数LEFT JOIN加status IN (0,1)保证了只有正常和迟到算作出勤缺勤和请假不计入。统计时CASE WHEN ... THEN ... END会先筛选出今天有记录的人再去重如果某学生缺勤a.status是2自然不会被算进present_today。报表接口的响应结构通常是这样的courseName,className,totalStudents,presentToday,absentCount,lateRate。前端拿到后直接用 ECharts 或 uCharts 画柱状图。答辩时这一层的完整链路数据库 SQL 到图表展示最容易拿“优秀”评价因为多数人的图表只是静态 mock 数据画出来的而这里是从真实考勤记录聚合而来。5. 避坑与常见问题定位漂移、日志不打印与接口联调的真实排查记录5.1 GPS 定位漂移几十米明明在教室却被判定为缺勤现象学生站在讲台边上手机上报的经纬度显示在隔壁宿舍楼后端距离计算超过 50 米签到直接被拒。原因有两层。一是手机冷启动定位第一次拿到的位置来自基站粗略估算精度可能 200-500 米二是安卓系统对getLocation的返回可能来自缓存缓存点可能是半小时前的位置。解决前端在success回调里判断res.accuracy大于 50 米时丢弃本次结果重新调用uni.getLocation并带timeout参数后端不要把单次定位结果作为最终结果结合签到时间窗宽松一个误差余量。实际处理代码getBetterLocation(tries 0) { uni.getLocation({ type: gcj02, accuracy: high, success: (res) { if (res.accuracy 50 tries 2) { this.getBetterLocation(tries 1); return; } this.latitude res.latitude; this.longitude res.longitude; } }); }5.2 WiFi 指纹在教室有信号但评分时好时坏现象同一个座位第一次签到成功第二次签到 WiFi 评分为负候被判定位失败。这就是 WiFi 评分里最玄学的地方AP 信号强度受人体遮挡和并发占用影响波动很大。原因把“匹配到 AP 就算成功”的阈值设得太严或者数据库里的基准 RSSI 是某一时刻采集的值不代表真实分布。解决采集基准值时在同一教室取 5 个不同角落、每个角落扫 3 次取平均评分函数里放宽偏差阈值到 12-15 dBm。更重要的是把 WiFi 判定定位成“兜底”而不是“必须条件”——GPS 已经通过的情况下不执行 WiFi 评分。5.3 UniApp 真机调试时 console.log 不打印日志信息现象HBuilderX 连接安卓真机代码里写了很多console.log控制台里什么都不显示定位是否成功也看不到。原因部分安卓机型在发布模式下会丢弃 console 输出HBuilderX 的真机调试模式未被正确连接日志走到了系统日志而不是开发工具面板。解决先在 HBuilderX 的“运行 → 运行到手机或模拟器”里确认设备列表有这台机器不要选择“打包后的安装包调试”代码里临时改用uni.showToast显示定位结果定位还不行就在 manifest.json 里把调试模式勾选打开重新同步资源。5.4 安卓 10/11 上定位权限申请了WiFi 扫描结果仍为空现象代码里有ACCESS_FINE_LOCATION权限权限弹窗也点了允许getScanResults()依然返回空数组。原因安卓 8.0 开始App 需要开启定位服务不是权限是系统定位开关才能扫描到 WiFi安卓 10/11 上即使开了定位后台扫描间隔也受到系统限制。解决提示用户必须打开系统定位开关签到页每次进入时检查定位服务是否开启扫描调用不要过于频繁至少间隔 3 秒以上抢扫会直接命中系统限频策略。5.5 接口联调时经纬度被截断后端距离严重失真现象前端明明传了30.123456后端打印出来却是30.1220 米的教室级判定变成了 200 米的校外判定。原因后端用float接收经纬度导致的精度衰减或者前端把数字转成了字符串且小数位被Number.toFixed(2)截断。解决后端 DTO 一律用Double接收数据库字段统一DECIMAL(10,6)前端不要自己对经纬度做toFixed()即使要格式化显示提交的原始维度和经度也要用完整数值。联调时在后端打印日志确认小数点后至少有 6 位小数。6. 进阶把毕业设计升级成能稳定运行的考勤产品毕业设计拿到“优秀”和让系统真正被老师用起来中间差一层验证和几个扩展点。先说验证方法。这套双定位系统到底准不准不能靠一两次签到感知建议在三个典型位置做量化测试教室中心、走廊、隔壁教室。每个位置让不同手机分别用 GPS、WiFi、双定位各签 5 次记录判定结果与实际位置的偏差。把结果画成表格就能看出来哪一路信号在哪个片区更可靠。这个表写进论文的“实验与分析”章节答辩含金量比贴代码高得多。测试位置 GPS(米) WiFi匹配率 双定位结论 教学楼301中心 8 3/3 全部通过 教学楼301走廊 15 2/3 通过 隔壁教室302 42 0/3 拒绝这个结果能支撑一个结论GPS 单路在空旷教室可用但有 40 米级误判风险双定位核心价值就是把这种误判率压到可接受范围。扩展方向上如果想让系统像商业产品一样可靠三个点值得做。第一引入蓝牙信标iBeacon作为第三路定位源教室角落放一个低功耗蓝牙模块手机扫码签到同时读蓝牙 MAC 地址室内定位精度能从 WiFi 的 10 米级提升到 3 米级代价是额外硬件成本。第二请假流程闭环现在系统里缺勤状态是终态的真正运行时需要学生在移动端提交请假申请、教师端审核后把状态改成3。第三批量签到性能课堂 200 人同一时间点击签到后端接口要能扛住至少确认数据库连接池上限和接口的 QPS 测试结果。最后说一个我自己的血泪经验这套系统里最容易被低估的是“坐标系”而不是定位算法当初我拿手机实测精度时wgs84 和 gcj02 的差异让我把一段定位代码整个删掉重写。读源码时先确认统一用哪个坐标系后端数据库、前端 SDK、地图文档三者必须一致否则后面所有距离计算都是白算。希望这次梳理能帮你把该避的坑提前避掉让双定位考勤系统真正跑起来。本文还有配套的精品资源点击获取