ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于安卓的学生签到系统:定位漂移与防代签的完整落地实践

基于安卓的学生签到系统:定位漂移与防代签的完整落地实践

简介:这是一套面向高校课程考勤与教学管理场景的安卓学生签到系统完整源码,适合计算机相关专业学生做课程设计、毕业设计或安卓与SSM框架练手。项目采用Android Studio开发移动端,后端基于SSM框架并在IntelliJ IDEA中搭建,支持扫码签到、数字签到、定位签到三种方式,签到记录可修改,另附加课码选课功能,覆盖考勤与选课两大教学管理需求。资源包共3个文件,包含2个rar压缩包与1个sql数据库文件,分别对应安卓前端工程、后端服务工程和初始数据库脚本,整体约139.01MB,解压后可直接导入部署。目前已有837人学习下载,读者可借此获得一套结构完整、功能可运行的前后端分离项目方案,快速理解移动端与SSM后端的接口对接、数据库表设计及签到逻辑实现,也可在此基础上二次开发或作为答辩演示素材。

1. 基于安卓的学生签到系统:从定位漂移到防代签的完整落地思路

做过校园信息化的人大多被同一个问题折磨过:老师拿着纸质名单挨个点名,一节课五分钟没了,数据还停留在纸上。基于安卓的学生签到系统要解决的就是这件事——把点名搬到手机端,让签到数据实时进库、可统计、可追溯。它适合两类人:一类是学校信息化岗的开发者,需要一套能跑在普通安卓机上的轻量方案;另一类是带课老师或学生干部,想自己搭一套不依赖第三方平台的小工具。但真正动手就会发现,难点从来不是"画个界面点个按钮",而是定位漂移、代签、弱网提交失败这三座大山。这篇笔记按我实际踩过的路径,从技术选型讲到参数调优,把每一步能复现的细节摊开说。

2. 技术选型:安卓端、服务端和定位方案怎么定

2.1 为什么优先选原生安卓而不是跨平台框架

标题锁死了"基于安卓",所以第一件事是确认技术栈。市面上做签到类应用,常见三条路:原生 Android(Kotlin/Java)、跨平台框架(Flutter/React Native)、以及套壳 WebView。我一般推荐原生,理由很实在:签到系统重度依赖定位、蓝牙、相机扫码、后台保活这些系统能力,跨平台框架在定位精度和后台权限上经常隔一层,出问题时排查成本高。原生直接调FusedLocationProviderClient,拿到的是系统融合定位结果,精度和功耗都可控。

服务端不用搞复杂,一个 Spring Boot 或 Node.js 的 REST 接口加 MySQL 就够了。核心表就三张:课程表、签到任务表、签到记录表。签到记录表里必须存经纬度、定位精度、设备标识、时间戳,这几个字段是后面防代签和申诉的依据。

提示:如果学校已有统一身份认证,登录环节直接对接,别自己再造一套账号体系,否则后期数据对不上是常态。

2.2 定位方案:GPS、基站、WiFi 三者怎么取舍

签到能不能用,八成取决于定位准不准。安卓的定位来源有三类:GPS(卫星)、基站(蜂窝)、WiFi(热点)。室内教室 GPS 信号弱,经常飘到几十米外;WiFi 定位在密集教学楼里反而更稳。实际做法是让系统自动融合,代码里设置定位优先级为高精度模式,同时接受这三种来源。

关键参数是"签到半径"。设太大,学生在隔壁楼也能签;设太小,站在教室后排都签不上。我的经验值是室内 50 到 80 米,室外 100 到 150 米。这个值不是拍脑袋,要结合定位精度动态判断——如果手机上报的accuracy是 30 米,半径却只给 20 米,那必然误判。

// 请求高精度定位,融合 GPS/WiFi/基站 val locationRequest = LocationRequest.Builder( Priority.PRIORITY_HIGH_ACCURACY, // 高精度模式 5000L // 5 秒更新一次,签到场景够用 ).apply { setMinUpdateDistanceMeters(5f) // 移动 5 米才回调,省电 setWaitForAccurateLocation(true) // 等待精度达标再返回 }.build() // 拿到位置后判断是否在签到半径内 fun isInRange( userLat: Double, userLng: Double, userAcc: Float, targetLat: Double, targetLng: Double, radius: Double ): Boolean { val results = FloatArray(1) Location.distanceBetween(userLat, userLng, targetLat, targetLng, results) val distance = results[0] // 把定位误差算进去,避免精度差导致误判 return distance <= radius + userAcc }

这段代码有两个要点。第一,setWaitForAccurateLocation(true)会让系统多等一会儿拿更准的结果,签到场景值得这点延迟。第二,判断距离时把userAcc(定位精度)加进半径,这是很多人忽略的地方——手机自己都承认误差 30 米,你却按精确点算,必然出问题。

2.3 服务端接口设计:一次签到要传哪些字段

接口设计决定了后面能不能防代签。签到请求不能只传"学生ID + 课程ID",那样谁都能伪造。至少要带上:经纬度、定位精度、设备唯一标识、客户端时间戳、以及一个服务端下发的动态令牌。

-- 签到记录表核心字段 CREATE TABLE sign_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_id BIGINT NOT NULL, student_id BIGINT NOT NULL, sign_time DATETIME NOT NULL, -- 服务端时间,不信客户端 latitude DOUBLE, -- 签到纬度 longitude DOUBLE, -- 签到经度 accuracy FLOAT, -- 定位精度(米) device_id VARCHAR(64), -- 设备标识,防一机多签 token VARCHAR(64), -- 动态令牌,防重放 status TINYINT DEFAULT 1, -- 1正常 2异常待审 UNIQUE KEY uk_course_student (course_id, student_id) );

sign_time一定要用服务端时间,客户端时间可以被改。device_id用来识别同一台设备给多个账号签到的情况。token是老师发起签到时服务端生成的,有效期设 2 到 5 分钟,过期作废,这样截个二维码慢慢传也来不及。

3. 动手实现:从发起签到到数据落库的完整链路

3.1 老师端发起签到:动态令牌怎么生成和刷新

老师点"发起签到",服务端做三件事:生成一个随机令牌、记录签到开始时间、设定有效期。令牌用 UUID 加时间戳哈希,别用自增 ID,否则能被猜。

// 服务端:发起签到,生成动态令牌 public String startSign(Long courseId, int validSeconds) { String token = UUID.randomUUID().toString().replace("-", ""); // 存 Redis,key 带课程维度,过期自动失效 String key = "sign:token:" + courseId; redisTemplate.opsForValue().set(key, token, validSeconds, TimeUnit.SECONDS); // 同时记录签到开始时间,用于判断迟到 redisTemplate.opsForValue().set( "sign:start:" + courseId, String.valueOf(System.currentTimeMillis()), validSeconds, TimeUnit.SECONDS ); return token; }

validSeconds我一般设 120 到 300 秒。太短学生来不及操作,太长代签空间大。令牌存 Redis 而不是数据库,是因为它天然带过期,不用自己写清理逻辑。老师端界面把这个令牌转成二维码显示,学生扫码后带着令牌提交签到。

3.2 学生端签到流程:扫码、定位、提交三步

学生端流程要顺,任何一步卡住都会有人放弃。顺序是:扫码拿令牌 → 请求定位 → 组装数据提交。定位这步要加超时和降级,不能无限等。

// 学生端:扫码后执行签到 suspend fun doSign(token: String, courseId: Long): SignResult { // 1. 拿定位,最多等 8 秒 val location = withTimeoutOrNull(8000L) { getCurrentLocation() } ?: return SignResult.Fail("定位超时,请到窗边重试") // 2. 组装请求体 val body = mapOf( "courseId" to courseId, "token" to token, "latitude" to location.latitude, "longitude" to location.longitude, "accuracy" to location.accuracy, "deviceId" to getDeviceId() ) // 3. 提交,失败进本地队列等重试 return try { val resp = api.sign(body) if (resp.code == 0) SignResult.Success else SignResult.Fail(resp.msg) } catch (e: IOException) { LocalQueue.enqueue(body) // 弱网兜底 SignResult.Pending("网络异常,已缓存待提交") } }

withTimeoutOrNull(8000L)是血泪经验——不设超时,室内定位偶尔要等半分钟,学生以为卡死了就退出重来。8 秒拿不到就提示换位置。弱网时把请求塞进本地队列,等网络恢复再补交,这是保证签到率的关键。

3.3 服务端校验:五道关卡拦住异常签到

收到请求后不能直接入库,要过五道校验。任何一道不过,标记为异常待审而不是直接拒绝,给申诉留口子。

校验项判断逻辑不通过处理
令牌有效性Redis 中是否存在且未过期直接拒绝
时间窗口服务端时间在签到有效期内标记迟到
距离范围距离 ≤ 半径 + 精度标记异常待审
设备唯一同课程同设备只允许一个账号标记异常待审
重复签到唯一索引冲突返回已签到
// 服务端校验核心逻辑 public SignResult verify(SignRequest req) { // 1. 令牌校验 String cached = redisTemplate.opsForValue() .get("sign:token:" + req.getCourseId()); if (cached == null || !cached.equals(req.getToken())) { return SignResult.reject("签到已结束或令牌无效"); } // 2. 距离校验,把精度算进去 double dist = GeoUtil.distance( req.getLatitude(), req.getLongitude(), course.getLatitude(), course.getLongitude() ); boolean inRange = dist <= course.getRadius() + req.getAccuracy(); // 3. 设备校验 boolean deviceUsed = recordMapper.existsByCourseAndDevice( req.getCourseId(), req.getDeviceId() ); // 任一异常都入库但标记待审,不直接拒绝 int status = (inRange && !deviceUsed) ? 1 : 2; recordMapper.insert(buildRecord(req, status)); return SignResult.ok(status); }

注意这里"标记待审"而不是"直接拒绝"的设计。定位偶尔飘、设备换新机,直接拒绝会误伤真实学生,留个待审让老师人工判断,体验好很多。

4. 避坑与排查:那些让签到系统翻车的细节

4.1 定位一直返回 null 或精度极差

现象:学生反馈"一直提示定位超时",日志里location为 null 或accuracy上百米。原因通常是权限没给全——安卓 6.0 以后定位权限分粗略和精确两级,只申请粗略权限拿不到 GPS。解决:在 Manifest 里同时声明ACCESS_FINE_LOCATION和ACCESS_COARSE_LOCATION,运行时两个一起请求。另外室内要提示用户开 WiFi,纯 GPS 在楼里基本没戏。

4.2 同一台手机给多个账号签到

现象:一个学生拿室友手机挨个签。原因:只校验了账号没校验设备。解决:device_id用Settings.Secure.ANDROID_ID加应用签名哈希生成,同一课程内限制一台设备只能签一个账号。注意别用 IMEI,新系统拿不到。

4.3 弱网下签到请求丢失

现象:学生说"我点了签到但系统没记录"。原因:请求发出后网络断了,客户端没做重试。解决:本地用 Room 建个待提交队列,提交失败先落库,起个 WorkManager 定时任务在网络恢复后补交,补交时带上原始时间戳,服务端按时间戳判断是否在有效期内。

4.4 服务端时间与客户端时间对不上

现象:明明在有效期内签到,却被判迟到。原因:用了客户端时间做判断。解决:所有时间判断以服务端为准,客户端时间只做展示。签到有效期从服务端发起那一刻算起,别信手机时钟。

4.5 二维码被拍照外传代签

现象:老师把二维码投屏,有人拍照发给没来的人。解决:令牌有效期压到 2 分钟以内,并且令牌绑定发起签到的设备或教室 WiFi 的 BSSID,扫码时校验 BSSID 是否一致。这招能挡住大部分远程代签。

5. 进阶技巧:把签到数据用起来和防代签的最后一公里

签到系统做完基础功能只是及格,真正拉开差距的是数据利用和防代签深度。先说数据。签到记录表积累一学期后,能算出每个学生的出勤率、迟到频次、以及"总是踩点签到"的行为模式。我一般会加一个统计接口,按课程和学生两个维度聚合,老师一眼能看到谁需要关注。SQL 层面用GROUP BY加条件聚合就够,不用上大数据那套。

-- 按学生统计出勤情况 SELECT student_id, COUNT(*) AS total, SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) AS normal, SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) AS abnormal, ROUND(SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) / COUNT(*), 3) AS rate FROM sign_record WHERE course_id = ? GROUP BY student_id ORDER BY rate ASC;

再说防代签的最后一公里。前面讲的令牌、设备、BSSID 能挡住大部分,但挡不住"人来了手机没来"或者"手机来了人没来"。进阶做法是加一步活体校验:签到成功后随机要求拍一张现场照片,或者做一次人脸比对。照片方案成本低,但要注意隐私合规,只存特征不存原图,用完即删。人脸比对要接第三方能力,小项目不一定值得。

还有一个容易被忽略的点:签到系统的"后悔药"机制。学生确实到了但定位飘了,得有个申诉入口,让他上传一张现场照片或让老师手动补签。这个入口不做,客服压力会全压到你身上。我的习惯是异常记录自动进待审列表,老师端一键批量处理,比让学生挨个找你强得多。

最后说个验证方法:上线前别只测自己一个人。找三五个同学,一个在教室、一个在隔壁楼、一个在操场,同时签到,看系统判断是否符合预期。再模拟一次断网签到,确认补交队列能正常工作。这套流程跑通,基本就能交付了。我自己第一次做的时候没测弱网,结果正式用的时候一半人签到失败,那场面至今记得。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进