简介:基于安卓的学生签到系统是一套面向高校课堂考勤场景的完整项目源码,适合Android开发者、SSM后端学习者及毕业设计参考。项目覆盖扫码签到、数字签到、定位签到三种考勤方式,并支持签到记录修改与加课码选课功能,可满足日常教学管理中的灵活考勤需求。资源包共3个文件,约139MB,包含两个rar压缩包和一个sql数据库文件:后端基于IntelliJ IDEA开发,前端基于Android Studio构建,sql文件为初始数据库,便于快速还原环境。整体结构清晰,源码、配置与数据分离,读者可参照作者博客中的部署流程完成服务器端上线。目前已有838人学习下载,适合有一定Java与Android基础、希望获得完整前后端联调实践的中级学习者。通过本项目可掌握SSM框架整合、移动端网络请求、数据库设计及定位扫码功能实现等关键技能。
1. 基于安卓的学生签到系统:一次从服务端到手机端串起来的实战拆解
一份基于安卓的学生签到系统,通常不是一个人纯手搓出来的,而是技术栈和工具的合谋:后端用 SSM(Spring、SpringMVC、MyBatis)提供签到接口,安卓端用 Android Studio 写界面与网络请求,再配合 IntelliJ IDEA 做服务端代码调试和打包。这套组合在毕业设计、课程设计和中小型实训项目里非常常见。它解决的是课堂签到场景下的两个刚需:一是教师能快速发起签到并拿到出勤统计,二是学生用手机完成学号登录、选课和定位签到,整个过程不再依赖纸质表格。如果你正准备搭一套这样的系统,又想避开那些只有跑起来才发现的坑,这篇笔记值得看完再动手。
2. 服务端 SSM 骨架:三层分包、四张表和一个签到接口的通路
2.1 为什么选 SSM 而不是别的组合
服务端选型时,我一般先看团队熟悉度和项目体量。SSM 三件套的分工很清晰:Spring 管对象和事务,SpringMVC 管请求路由,MyBatis 管 SQL。对比 Spring Boot + MyBatis,SSM 的 XML 配置虽然繁琐,但每个 bean 的来源和注入方式都能看到,出了问题也好定位。对于签到这种请求量不大、逻辑不复杂的系统,SSM 完全够用,而且很多课程设计模板本身就是 SSM,资料好找。
安卓端选 Java 而不是 Kotlin,也考虑到了和现有代码模板的兼容性。网络层用 OkHttp,JSON 解析用 Gson,这两个库足够覆盖登录、课程列表、签到三个核心请求。不要一上来就引全家桶,签到系统的痛点从来不在功能多,而在链路长——手机、服务端、数据库三层任何一处出问题,整个流程就断了。
2.2 服务端工程结构与双容器配置
标准的 SSM Maven 项目,我一般这样分包:
- controller:接收安卓端请求,返回 JSON
- service:签到逻辑、登录校验
- mapper:MyBatis 接口与 XML
- entity:学生、课程、签到记录实体
- resources:spring-mvc.xml、applicationContext.xml、mybatis-config.xml
关键在 spring-mvc.xml 的配置。一个典型的写法如下:
<context:component-scan base-package="cn.campus.controller"/> <mvc:annotation-driven> <mvc:message-converters> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"> <property name="objectMapper" ref="jacksonObjectMapper"/> </bean> </mvc:message-converters> </mvc:annotation-driven>这里有两处容易踩坑:controller 扫描包和 service 扫描包必须分开写在两个配置文件里,否则 SpringMVC 会把 service 也扫进 Web 容器,造成事务注解失效。另一个是 message-converters,安卓端要的是纯 JSON,不配 Jackson 转换器,返回的可能是 view 名称而不是数据。
applicationContext.xml 里我只扫描 service 和 mapper,数据源用 dbcp 或者 druid,事务管理器指向 DataSource。这样 Spring 容器管业务,SpringMVC 容器只管 web 层,两者各司其职,后续排查 bean 找不到的问题会省很多时间。
2.3 四张表的设计:从学生到签到记录
签到系统的核心表,我通常会拆成四张:
| 表名 | 关键字段 | 说明 |
|---|---|---|
| student | id、学号、姓名、班级、密码 | 学生账号 |
| teacher | id、工号、姓名、密码 | 教师账号 |
| course | id、课程名、教师id、签到码 | 课程与签到码 |
| sign_record | id、学号、课程id、签到时间、经度、纬度、状态 | 学生签到记录 |
course 表里的签到码是每次发起签到时随机生成的六位数字,学生必须在签到码有效期内输入才能完成签到。严格来说,course 表还应该加签到开始时间和截止时间,这两个时间字段是后面判断迟到和缺勤的依据。
sign_record 表要加一个唯一约束,建议用 (student_id, course_id) 做联合唯一索引,避免同一学生同一课程重复签到产生多条脏数据。这个约束在实际运行中非常重要,后面会单独展开。
2.4 签到接口实现:状态判断与幂等处理
签到的核心逻辑放在 service 层,接口定义大致如下:
public interface SignService { Result sign(SignRequest request); }SignRequest 包含学号、课程id、签到码、经度和纬度。service 实现里依次做三步校验:查课程是否存在,判断当前时间是否在有效期内,然后按唯一约束插入签到记录。
Course course = courseMapper.findById(req.getCourseId()); if (course == null) { return Result.error("课程不存在"); } long now = System.currentTimeMillis(); if (now < course.getStartTime()) { return Result.error("签到尚未开始"); } SignRecord record = new SignRecord(); record.setStudentId(req.getStudentId()); record.setCourseId(req.getCourseId()); record.setSignTime(new Date(now)); record.setLatitude(req.getLatitude()); record.setLongitude(req.getLongitude()); record.setStatus(now <= course.getEndTime() ? 1 : 2); try { signRecordMapper.insert(record); } catch (DuplicateKeyException e) { return Result.error("请勿重复签到"); } return Result.success();这段逻辑里有三个细节值得说明。第一,status 字段用 1 表示正常、2 表示迟到、0 表示缺勤,状态码不要设计成字符串,后面统计聚合会非常痛苦。第二,用 DuplicateKeyException 捕获重复签到而不是先 select 再 insert,因为两步操作在并发下会有竞态,直接靠数据库唯一约束兜底最可靠。第三,这里没有做经纬度校验,而是把原始经纬度存进库,判断交给安卓端或者统计端,避免服务端过度耦合定位逻辑。
2.5 统计接口:聚合查询的简化处理
教师端需要看到某门课程的出勤率,我一般提供两个接口:一个是按课程汇总的统计接口,另一个是导出学生签到明细。汇总接口的 SQL 用 group by 即可:
<select id="countSignByCourse" resultType="map"> SELECT status, COUNT(*) as count FROM sign_record WHERE course_id = #{courseId} GROUP BY status </select>这里返回的是 Map,安卓端拿到后自己组装展示。分组统计的好处是把逻辑下沉到 SQL,服务端不用写循环和判断。这个接口不需要分页,因为 status 只有三种,结果走不到十条记录。
3. 安卓端从零接通:登录、课程列表与签到动作的联调路径
3.1 工程操作:IntelliJ IDEA 与 Android Studio 的配合方式
安卓端工程和服务端工程,我一般开两个窗口:服务端代码在 IntelliJ IDEA 里跑,安卓端在 Android Studio 里跑。实际开发中,很多人习惯把两个工程放在同一个目录下,用 IDEA 直接打开安卓端,但这会遇到 Gradle 插件和 IDE 版本不匹配的问题。
我推荐的做法是:服务端用 IntelliJ IDEA 的 Tomcat 配置启动,安卓端用 Android Studio 的模拟器跑,两边通过 HTTP 接口通信。代码层面,安卓端创建一个独立的 module,依赖引入 OkHttp、Gson 和 RecyclerView,不和服务端共享任何代码。这样两个端可以独立构建,任一端报错不会互相影响。
3.2 网络层封装:OkHttp 的同步请求与 JSON 解析
安卓端的网络请求,我习惯封装一个简单的 HttpUtil:
public class HttpUtil { private static OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build(); public static String postJson(String url, String json) throws IOException { RequestBody body = RequestBody.create( MediaType.parse("application/json; charset=utf-8"), json); Request request = new Request.Builder() .url(url) .post(body) .build(); try (Response response = client.newCall(request).execute()) { return response.body().string(); } } }这段封装很基础,但足够用。connectTimeout 和 readTimeout 都设成 10 秒,校园网老化时不会让界面卡太久。postJson 返回的是字符串,解析工作交给调用方用 Gson 处理。要注意的是 OkHttp 的 execute() 是同步方法,不能直接在主线程调用,需要放到子线程里。
3.3 登录与课程列表
登录页的逻辑很直接:用户输入学号和密码,点击登录后提交到 /api/student/login。服务端校验通过后返回学生姓名和班级,安卓端把这两个字段存到 SharedPreferences,后续请求只需要带学号。
课程列表的请求是登录后自动发起的。列表页用 RecyclerView 展示,每行显示课程名、教师名和签到状态。这里有一个体验层面的细节:列表接口返回的字段里,有一个 isSigned 字段表示当前学生是否已经签过这门课,这个字段在 service 层用子查询算好返回,安卓端直接读取,不需要自己维护签到状态。
3.4 签到动作与状态反馈
点击签到按钮后,安卓端需要做三件事:获取经纬度、读取当前课程id、组装请求体。定位获取的代码片段如下:
LocationManager lm = (LocationManager) getSystemService(LOCATION_SERVICE); Location location = lm.getLastKnownLocation(LocationManager.GPS_PROVIDER); if (location == null) { location = lm.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); } double latitude = location != null ? location.getLatitude() : 0; double longitude = location != null ? location.getLongitude() : 0;getLastKnownLocation 返回的是最后一次定位结果,响应速度快但可能过期。签到场景对实时性要求不高,用这个方法是划算的。拿到定位后组装 JSON:
{"studentId": "20240101", "courseId": 5, "latitude": 39.9042, "longitude": 116.4074}服务端返回的 Result 结构里带 code 和 message,安卓端根据 code 判断弹窗内容。这里要注意不能只用 Toast 显示一层含义,比如迟到和成功都是签到完成,但状态码不同,界面提示应该区分开,否则学生会以为迟到也算正常签到。
4. 高频坑位排查:五个常见故障的现象、原因与修复
4.1 模拟器里永远连不上服务端:localhost 指向了自己
现象:Android Studio 模拟器里打开登录页,点击登录后一直转圈,Logcat 里报 ConnectException,服务端 Tomcat 控制台没有任何请求进来。
原因:模拟器是独立虚拟机,它自己的 localhost 指向模拟器本身,不是开发机的 127.0.0.1。安卓端代码里写 localhost:8080,实际上是在模拟器内部找服务,必然连接失败。
解决:把服务端地址换成 10.0.2.2,这是 Android 官方模拟器访问宿主机环回地址的固定规则。代码里网络层封装成常量,全局只改一处即可。
4.2 手机连接正常但请求被拦截:明文 HTTP 流量默认禁止
现象:真机调试时,请求发出去了,服务端也能收到,但返回后安卓端报 IOException,控制台提示 Cleartext HTTP traffic not permitted。
原因:Android 9 开始默认禁止应用使用明文 HTTP 协议。开发和测试阶段用 http:// 访问局域网服务端,属于明文流量,会被系统直接拦截。
解决:在 AndroidManifest.xml 的 application 标签里配置 android:usesCleartextTraffic="true"。如果项目已经上了 networkSecurityConfig,只需要在 config 文件里把局域网网段加入允许列表。不建议长期开着全局明文,但签到系统的开发阶段可以这么处理。
4.3 MyBatis 报 Invalid bound statement:XML 没被编译进 classpath
现象:启动 Tomcat 后请求签到接口,控制台报 org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)。
原因:最常见的场景是 mapper 接口和 XML 文件放在了同一个 package 下,但 Maven 默认只编译 .java 文件,写在 src/main/java 目录里的 XML 资源不会被输出到 target/classes,导致运行期找不到 SQL 映射。
解决:把 mapper 的 XML 文件放到 src/main/resources/mapper 目录下,并在 applicationContext.xml 里配置 mapperLocations 为 classpath:mapper/*.xml。如果一定要和接口放一起,需要在 pom.xml 的 build 节点里加上 resource 配置,把 xml 文件一并打包。
4.4 JSON 解析错乱:日期字段序列化成了数字数组
现象:安卓端 Gson 解析签名字段报错,或者解析出来的日期是一串[2024, 9, 10, 8, 30, 0]这样的数组。
原因:Java 8 的 LocalDateTime 在 Jackson 默认序列化规则下,输出的是年月日时分秒的数组结构,Gson 没做适配就直接懵了。
解决:在服务端统一配置 JavaTimeModule 和日期格式,指定 offsetDateTime 的格式为 yyyy-MM-dd HH:mm:ss。或者干脆实体类里不用 LocalDateTime,改用 JDK 的 java.util.Date,配合 Jackson 自带的 dateFormat 配置,也能省掉额外依赖。
4.5 定位距离判断不生效:浮点经纬度直接比较是伪命题
现象:学生人明明在教室附近,服务端却判定签到定位不通过,或者反过来,人不在教室也能签上。
原因:GPS 传回来的经纬度是浮点数,两次定位之间本身就存在米的级抖动。直接用 equals 或者 == 比较经纬度相等,是最典型的想当然写法。签到系统需要的是距离阈值,不是坐标相等。
解决:服务端存下教师发起签到时设定的经纬度,学生签到时在 service 层计算球面距离,距离小于 500 米判定为有效。球面距离公式用 Haversine 公式,Java 里几行代码就能算完。这个校验放服务端做,因为客户端很容易被绕过。
5. 用虚拟定位把整套签到流程验证一遍:一个能复用的两步验证法
签到系统的联调,最烦人的不是代码逻辑,而是定位数据不可控。每次跑真机,GPS 结果都不一样,出了问题也搞不清是定位的问题还是接口的问题。我后来养成了一个习惯:先写死,再放开。
所谓先写死,就是调试阶段把经纬度在安卓端直接写死成服务端预设的值,比如 latitude 固定 39.9042,longitude 固定 116.4074。这样跑通的是整条链路——登录、课程列表、签到请求、数据库插入、状态码回传。链路通了,才有资格去调定位。
第二步是放开定位。在模拟器的扩展控制面板里设置一个固定的虚拟位置,模拟器会把该位置的 GPS 数据持续提供给应用。设置完之后重新走一遍签到流程,观察上传的经纬度是否落在教师设定的范围内。两步验证的好处是:如果放开定位后签到失败,你立刻能判断问题出在定位获取或距离计算,而不是网络或数据库层。
我平时还会加一个教学场景的验证手段:把签到码提前发到学生端,教师端发起签到时将签到码和截止时间一并返回,安卓端在输入框内校验。为了偷懒,我有时候会把课程 id 写进签到码里,前端解析签到码拿到 courseId,省掉一次下拉选课的操作。这种情况仅适合核心链路测试,真实使用还是建议走列表选课,避免课程和签到码的耦合关系在后期维护时变成负担。
从那以后,我每次搭签到系统,都强制自己走一遍先写死、再放开的两步验证流程,定位相关的翻车率大大降低。希望帮到你。
本文还有配套的精品资源,点击获取