简介基于安卓的学生签到系统是一套面向高校课堂考勤场景的完整项目源码适合Android开发者、SSM后端学习者及毕业设计参考。项目覆盖扫码签到、数字签到、定位签到三种考勤方式并支持签到记录修改与加课码选课功能可满足日常教学管理中的灵活考勤需求。资源包共3个文件约139MB包含两个rar压缩包和一个sql数据库文件后端基于IntelliJ IDEA开发前端基于Android Studio构建sql文件为初始数据库便于快速还原环境。整体结构清晰源码、配置与数据分离读者可参照作者博客中的部署流程完成服务器端上线。目前已有838人学习下载适合有一定Java与Android基础、希望获得完整前后端联调实践的中级学习者。通过本项目可掌握SSM框架整合、移动端网络请求、数据库设计及定位扫码功能实现等关键技能。1. 基于安卓的学生签到系统一次从服务端到手机端串起来的实战拆解一份基于安卓的学生签到系统通常不是一个人纯手搓出来的而是技术栈和工具的合谋后端用 SSMSpring、SpringMVC、MyBatis提供签到接口安卓端用 Android Studio 写界面与网络请求再配合 IntelliJ IDEA 做服务端代码调试和打包。这套组合在毕业设计、课程设计和中小型实训项目里非常常见。它解决的是课堂签到场景下的两个刚需一是教师能快速发起签到并拿到出勤统计二是学生用手机完成学号登录、选课和定位签到整个过程不再依赖纸质表格。如果你正准备搭一套这样的系统又想避开那些只有跑起来才发现的坑这篇笔记值得看完再动手。2. 服务端 SSM 骨架三层分包、四张表和一个签到接口的通路2.1 为什么选 SSM 而不是别的组合服务端选型时我一般先看团队熟悉度和项目体量。SSM 三件套的分工很清晰Spring 管对象和事务SpringMVC 管请求路由MyBatis 管 SQL。对比 Spring Boot MyBatisSSM 的 XML 配置虽然繁琐但每个 bean 的来源和注入方式都能看到出了问题也好定位。对于签到这种请求量不大、逻辑不复杂的系统SSM 完全够用而且很多课程设计模板本身就是 SSM资料好找。安卓端选 Java 而不是 Kotlin也考虑到了和现有代码模板的兼容性。网络层用 OkHttpJSON 解析用 Gson这两个库足够覆盖登录、课程列表、签到三个核心请求。不要一上来就引全家桶签到系统的痛点从来不在功能多而在链路长——手机、服务端、数据库三层任何一处出问题整个流程就断了。2.2 服务端工程结构与双容器配置标准的 SSM Maven 项目我一般这样分包controller接收安卓端请求返回 JSONservice签到逻辑、登录校验mapperMyBatis 接口与 XMLentity学生、课程、签到记录实体resourcesspring-mvc.xml、applicationContext.xml、mybatis-config.xml关键在 spring-mvc.xml 的配置。一个典型的写法如下context:component-scan base-packagecn.campus.controller/ mvc:annotation-driven mvc:message-converters bean classorg.springframework.http.converter.json.MappingJackson2HttpMessageConverter property nameobjectMapper refjacksonObjectMapper/ /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 四张表的设计从学生到签到记录签到系统的核心表我通常会拆成四张表名关键字段说明studentid、学号、姓名、班级、密码学生账号teacherid、工号、姓名、密码教师账号courseid、课程名、教师id、签到码课程与签到码sign_recordid、学号、课程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 idcountSignByCourse resultTypemap 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 解析安卓端的网络请求我习惯封装一个简单的 HttpUtilpublic 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; charsetutf-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:usesCleartextTraffictrue。如果项目已经上了 networkSecurityConfig只需要在 config 文件里把局域网网段加入允许列表。不建议长期开着全局明文但签到系统的开发阶段可以这么处理。4.3 MyBatis 报 Invalid bound statementXML 没被编译进 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.9042longitude 固定 116.4074。这样跑通的是整条链路——登录、课程列表、签到请求、数据库插入、状态码回传。链路通了才有资格去调定位。第二步是放开定位。在模拟器的扩展控制面板里设置一个固定的虚拟位置模拟器会把该位置的 GPS 数据持续提供给应用。设置完之后重新走一遍签到流程观察上传的经纬度是否落在教师设定的范围内。两步验证的好处是如果放开定位后签到失败你立刻能判断问题出在定位获取或距离计算而不是网络或数据库层。我平时还会加一个教学场景的验证手段把签到码提前发到学生端教师端发起签到时将签到码和截止时间一并返回安卓端在输入框内校验。为了偷懒我有时候会把课程 id 写进签到码里前端解析签到码拿到 courseId省掉一次下拉选课的操作。这种情况仅适合核心链路测试真实使用还是建议走列表选课避免课程和签到码的耦合关系在后期维护时变成负担。从那以后我每次搭签到系统都强制自己走一遍先写死、再放开的两步验证流程定位相关的翻车率大大降低。希望帮到你。本文还有配套的精品资源点击获取