基于SSM框架与Android的扫码签到系统:从架构设计到数据落地
简介基于安卓的学生签到系统是一份面向高校课堂考勤场景的完整项目资源适合Android开发学习者、毕业设计或课程设计使用者。系统基于SSM后端框架与Android Studio前端实现覆盖扫码签到、数字签到、定位签到三种方式并支持对签到记录进行修改还附带加课码选课功能实用性强。资源包共3个文件包含两个RAR工程压缩包与一个SQL初始数据库文件整体大小约139.01MB其中一个RAR对应Android前端工程另一个为IntelliJ IDEA下的后端工程SQL文件可帮助快速初始化数据库目录结构清晰便于导入与二次开发。已有838人学习下载尤其适合需要参考完整前后端交互、定位与扫码逻辑或希望在此基础上扩展功能的开发者整体结构前后端分离也可作为课程设计或毕业设计的参考基础。1. 基于安卓的学生签到系统从扫码到数据落库的一次完整闭环大学课堂的签到一直是考勤管理的痛点传统点名浪费时间代签现象又很难杜绝。这个基于安卓的学生签到系统采用 Android Studio 作为客户端开发工具服务端基于 SSMSpring Spring MVC MyBatis框架配合 IntelliJ IDEA 进行后端调试实现了一套从学生扫码、教师端实时查看到考勤数据落库的完整流程。适合计算机专业做毕业设计、课程设计也适合想系统学习 SSM 与 Android 联调的开发者参考。整个项目不是单一 App而是包含了服务端接口、管理后台和安卓客户端三部分跑起来之后能直接体验到完整的业务链路。2. 系统架构与工程结构客户端和服务端如何分工协作2.1 整体技术选型为什么是 SSM 而不是 Spring Boot选择 SSM 而不是 Spring Boot主要原因是教学场景和毕业设计中对框架原理的考察需求。SSM 的配置是显式可见的Spring MVC 的处理器映射、MyBatis 的 SQL 绑定都能在 XML 文件中直观看到这对理解框架运作机制很有帮助。Spring Boot 虽然开发效率高但大量自动化配置把细节封装成了黑匣子反而不利于学习。服务端采用经典的三层架构Controller 层接收安卓客户端的 HTTP 请求Service 层处理业务逻辑Mapper 层通过 MyBatis 操作 MySQL 数据库。安卓客户端则按照简易 MVP 思想组织Activity 负责界面展示和用户交互数据请求统一封装在独立的网络模块中。2.2 Android 客户端工程目录按功能模块分包客户端工程是典型的 Android Studio 项目结构业务代码集中在 app/src/main/java 目录下。包名按功能拆分为活动、适配器、网络、模型和工具五个部分每个包职责单一后期维护时定位问题非常快。app/src/main/java/com/example/signin/ ├── activity/ │ ├── LoginActivity.java // 学生登录 │ ├── TeacherMainActivity.java // 教师主页展示签到统计 │ └── StudentSignActivity.java // 学生扫码签到页 ├── adapter/ │ └── SignRecordAdapter.java // 签到记录列表适配器 ├── network/ │ ├── ApiClient.java // 网络请求客户端封装 │ ├── ApiService.java // 定义接口方法 │ └── ApiResponse.java // 统一响应实体 ├── model/ │ ├── Student.java // 学生实体 │ ├── Course.java // 课程实体 │ └── SignRecord.java // 签到记录实体 └── util/ ├── SharedPrefsUtil.java // 本地缓存登录状态 └── QRCodeScanner.java // 二维码扫描工具类每个 Activity 只负责自己的业务场景比如 LoginActivity 只处理登录请求和 Token 存储StudentSignActivity 只负责扫码和提交签到。这种按业务分包的方式比按技术类型分包更容易理解尤其适合多人协作开发。2.3 服务端工程目录SSM 标准分层服务端采用 Maven 管理依赖标准的 SSM 工程结构。使用 IntelliJ IDEA 打开后可以直接导入为 Maven 项目等待依赖下载完成即可启动。signin-server/ ├── src/main/java/com/example/signin/ │ ├── controller/ │ │ ├── StudentController.java // 学生相关接口 │ │ ├── TeacherController.java // 教师相关接口 │ │ └── SignController.java // 签到核心接口 │ ├── service/ │ │ ├── SignService.java // 签到业务逻辑接口 │ │ └── impl/SignServiceImpl.java │ ├── mapper/ │ │ ├── StudentMapper.java │ │ ├── CourseMapper.java │ │ └── SignRecordMapper.java │ └── entity/ │ ├── Student.java │ ├── Course.java │ └── SignRecord.java ├── src/main/resources/ │ ├── spring-mvc.xml // Spring MVC 配置 │ ├── spring-mybatis.xml // 数据源和 MyBatis 配置 │ ├── mybatis/ │ │ ├── StudentMapper.xml │ │ ├── CourseMapper.xml │ │ └── SignRecordMapper.xml │ └── jdbc.properties // 数据库连接配置 └── pom.xml2.4 数据库设计三张核心表的关系签到系统的核心数据模型围绕学生、课程和签到记录展开。学生表存储学生的基本信息和登录凭证课程表存储开课信息签到记录表则是整个系统的核心业务表关联学生和课程。这里给出核心的建表语句。CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) UNIQUE NOT NULL, student_name VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, created_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(100) NOT NULL, teacher_id INT NOT NULL, generate_code VARCHAR(64) UNIQUE NOT NULL, start_time DATETIME, end_time DATETIME ); CREATE TABLE sign_record ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, sign_time DATETIME DEFAULT CURRENT_TIMESTAMP, sign_status TINYINT DEFAULT 1, UNIQUE KEY uk_student_course (student_id, course_id) );sign_record 表的联合唯一索引是关键设计它保证了同一个学生对同一门课程只能签到一次从数据库层面杜绝了重复提交的问题。generate_code 字段存储课程对应的二维码内容每次发起签到时重新生成学生扫码后服务端会根据这个 code 定位到对应的课程。3. 运行环境准备与项目启动从零到能跑通全流程3.1 开发工具版本选择这步最容易翻车这个项目的运行依赖三个核心工具Android Studio、IntelliJ IDEA 和 MySQL。Android Studio 建议使用 4.x 以上版本配套的 Android SDK 版本建议 28 以上。IntelliJ IDEA 使用 2021 版本以上需要配置好 Maven 3.6 和 JDK 1.8。MySQL 使用 5.7 或 8.0 均可5.7 兼容性更好8.0 需要注意数据库驱动版本。血泪经验JDK 版本是最大的坑。SSM 框架对 JDK 版本的兼容性非常敏感用 JDK 11 编译 JDK 1.8 的项目经常会出现各种奇怪的反射异常。建议直接用 JDK 1.8 跑服务端Android Studio 内部默认的 JDK 版本不会影响服务端两侧独立使用互不干扰。3.2 MySQL 数据库初始化数据库初始化是第一步。先创建数据库然后导入项目提供的 SQL 文件即可。如果没有现成的 SQL 文件就按第二章给出的三张表结构手动执行建表语句然后插入一条测试数据。mysql -u root -p CREATE DATABASE signin_system DEFAULT CHARACTER SET utf8mb4; USE signin_system; source /path/to/signin.sql;utf8mb4 字符集是必须的如果用了 utf8 字符集保存表情符号时会报错。数据库字符集选错导致的乱码问题在后期联调时排查起来非常耗时。建表完成后可以手动插入一个学生账号和一个教师账号用于测试登录。3.3 服务端启动流程IntelliJ IDEA 配置 Tomcat在 IntelliJ IDEA 中导入 Maven 项目后需要配置 Tomcat 才能运行。这里的核心配置点是修改 jdbc.properties 中的数据库连接信息包括账号、密码和 URL。jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/signin_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456配置完成后在 IDEA 的 Run Configuration 中添加 Tomcat Server选择 Local 模式Deployment 中点击加号选择 Artifact类型选 war exploded。这里的玄学在于很多人直接点右上角的运行按钮发现 Tomcat 没有正确配置 Deployment导致访问接口一直 404。正确做法是 Deployment 里添加后Application Context 设置为/signin这样访问地址就是http://localhost:8080/signin/。启动成功后在浏览器访问http://localhost:8080/signin/api/ping如果返回 JSON 数据说明服务端已经跑通。3.4 安卓客户端连接服务端IP 地址配置技巧安卓客户端需要修改网络模块中的服务器地址。在 ApiClient.java 中将 BASE_URL 改成电脑的局域网 IP而不是本地回环地址。public class ApiClient { // 注意使用局域网IP模拟器访问电脑不能用127.0.0.1 private static final String BASE_URL http://192.168.1.100:8080/signin/; public static ApiService getApiService() { Retrofit retrofit new Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .build(); return retrofit.create(ApiService.class); } }这里的核心问题在于 Android 模拟器中的 127.0.0.1 指向的是模拟器自身而不是电脑。模拟器访问宿主机的正确方式是使用 10.0.2.2但如果用真机调试则需要填电脑的局域网 IP。还有一个常见问题是 Android 9 及以上默认禁止 HTTP 明文请求需要在 AndroidManifest.xml 中配置android:usesCleartextTraffictrue否则所有接口请求都会失败。4. 核心模块代码拆解签到业务从请求到入库的完整链路4.1 服务端签到接口的业务逻辑实现签到是系统的核心业务它的完整流程是学生扫码获取签到码然后提交到服务端服务端校验签到码有效性校验学生是否已签到最后写入数据库。这里看 SignServiceImpl 中的核心方法。Service public class SignServiceImpl implements SignService { Autowired private SignRecordMapper signRecordMapper; Autowired private CourseMapper courseMapper; Override public Result sign(String signCode, Integer studentId) { // 1. 根据签到码查找对应的课程 Course course courseMapper.selectByGenerateCode(signCode); if (course null) { return Result.error(签到码无效); } // 2. 校验签到时间是否在有效期内 Date now new Date(); if (now.before(course.getStartTime()) || now.after(course.getEndTime())) { return Result.error(不在签到时间范围内); } // 3. 校验是否重复签到 SignRecord existing signRecordMapper.selectByStudentAndCourse(studentId, course.getId()); if (existing ! null) { return Result.error(请勿重复签到); } // 4. 插入签到记录 SignRecord record new SignRecord(); record.setStudentId(studentId); record.setCourseId(course.getId()); record.setSignTime(now); record.setSignStatus(1); signRecordMapper.insert(record); return Result.success(签到成功); } }这段代码展示了典型的业务校验链。第一层校验签到码是否存在这能过滤掉乱扫的二维码第二层校验时间有效期防止学生在课程开始前或结束后提交签到第三层通过数据库的唯一索引做兜底校验。实际开发中前三步的校验顺序不能颠倒因为签到码查询是后续所有操作的前提。4.2 服务端二维码生成与课程绑定教师端发起签到时服务端生成一个随机字符串作为签到码创建课程记录并返回给客户端。客户端将签到码编码为二维码展示在屏幕上。这里用的是 UUID 的变体加上了时间戳做混淆。public String generateSignCode(Integer teacherId) { // 生成规则teacherId 时间戳 随机数然后取MD5后截取前16位 String raw teacherId _ System.currentTimeMillis() _ ThreadLocalRandom.current().nextInt(1000, 9999); String code MD5Util.md5(raw).substring(0, 16); return code; }这个生成规则的重点在于不可预测性。如果只用纯数字递增的签到码学生可以提前构造一个相邻的签到码完成签到形成一种变相的作弊。加入时间戳和随机数之后签到码的有效期由课程表里的 startTime 和 endTime 控制过期自动失效不需要额外设置状态位。4.3 客户端扫码模块的工程实现安卓端扫码使用了 ZXing 库核心逻辑封装在 QRCodeScanner 工具类中。扫码界面通过 SurfaceView 预览相机画面在回调中解析出二维码内容后自动发起签到请求。public class QRCodeScanner implements ZXingLiteScanner.Callback { private ZXingLiteScanner scanner; private OnScanResultListener listener; public void startScan(Activity activity) { scanner new ZXingLiteScanner(activity); scanner.setCallback(this); // 设置扫码框大小单位是像素 scanner.setScanBoxSize(240, 240); } Override public void onResult(String rawResult) { // rawResult 即签到码直接回调给业务层 if (listener ! null) { listener.onScanSuccess(rawResult); } } }扫码得到的 rawResult 就是服务端生成的签到码。拿到这个字符串后客户端调用 ApiService 中的 sign 接口将签到码和当前登录学生的 ID 一起提交。ZXingLiteScanner 封装了对相机权限的处理但在 Android 6.0 以上仍然需要在 Activity 中动态申请相机权限否则扫码界面会黑屏。这个权限属于运行时权限只在 AndroidManifest 中声明是不够的。4.4 客户端签到记录展示的适配器实现教师端查看签到情况是本系统的另一个核心功能。列表页使用 RecyclerView 展示所有学生的签到状态核心代码在 SignRecordAdapter 中。public class SignRecordAdapter extends RecyclerView.AdapterSignRecordAdapter.ViewHolder { private ListSignRecord records; Override public void onBindViewHolder(ViewHolder holder, int position) { SignRecord record records.get(position); holder.tvStudentName.setText(record.getStudentName()); holder.tvSignTime.setText(DateFormatUtils.format(record.getSignTime(), yyyy-MM-dd HH:mm:ss)); // 根据签到状态显示不同颜色 if (record.getSignStatus() 1) { holder.tvStatus.setText(已签到); holder.tvStatus.setTextColor(Color.parseColor(#4CAF50)); } else { holder.tvStatus.setText(未签到); holder.tvStatus.setTextColor(Color.parseColor(#F44336)); } } }这段代码的业务逻辑很简单但提示了一个开发习惯不要在适配器中直接操作数据库或发送网络请求。适配器的职责是绑定数据到视图数据的获取和计算应该放在更上层的 Activity 或 ViewModel 中。如果直接把 Retrofit 调用写在 onBindViewHolder 里数据刷新时会频繁触发请求同时内存泄漏的风险也会增加。5. 常见问题与避坑指南配置环境时容易翻车的五个细节5.1 项目导入后无法识别为 Maven 项目现象用 IntelliJ IDEA 打开服务端源码后pom.xml 没有被识别右键项目也没有 Maven 相关选项。原因是 IDEA 没有自动识别 Maven 工程类型或者导入时选择了错误的打开方式。解决直接关闭项目窗口回到 IDEA 欢迎页选择 Open定位到服务端根目录时不要直接点 OK而是选中 pom.xml 文件再点击 Open。在弹出的对话框中选择 Open as ProjectIDEA 就会自动识别并下载 Maven 依赖。如果依赖下载缓慢可以修改 Maven 的 settings.xml配置国内的镜像源。5.2 Android 9 以上设备无法访问 HTTP 接口现象客户端运行后所有网络请求都报 IOException错误信息是 CLEARTEXT communication not permitted。这个问题是因为 Android 9 开始系统默认禁止应用使用明文 HTTP 协议访问网络。解决在 AndroidManifest.xml 的 application 标签中添加android:usesCleartextTraffictrue。生产环境更规范的做法是配置 network-security-config只允许特定域名使用明文流量。但在本地调试阶段直接开启这个开关是最快的方案。5.3 模拟器无法访问电脑上的服务端接口现象使用 Android Studio 自带模拟器运行客户端登录时一直提示超时。很多人以为是服务端没启动或者 IP 地址配错了实际上问题出在模拟器的网络机制上。解决Android 模拟器中 127.0.0.1 指向模拟器自身要想访问宿主机必须使用 10.0.2.2。如果客户端跑在真机上则需要填写电脑在当前局域网内的 IP 地址。还有一点极易忽略电脑的防火墙可能拦截了 8080 端口的入站请求需要在防火墙中放行 Java 进程或直接开放 8080 端口。5.4 Linux 和 macOS 下 Tomcat 启动端口冲突现象启动服务端时Tomcat 报端口被占用错误。通常是因为电脑上已经运行了其他 Java 进程占用了 8080 端口。解决方法是先查看占用端口的进程再决定是杀掉进程还是修改 Tomcat 端口。# Windows下查看端口占用 netstat -ano | findstr 8080 # Linux/macOS 下查看端口占用 lsof -i :8080如果不想杀掉占用端口的进程可以直接修改 Tomcat 的 server.xml 中 Connector 的 port 属性改成 8081 或其他未被占用的端口。改了端口后记得同步修改安卓客户端里的 BASE_URL 和数据库连接配置。5.5 MyBatis XML 映射文件扫描不到现象服务端启动时没有报错但调用 Mapper 接口方法时抛出 Invalid bound statement (not found) 异常。原因是 MyBatis 在初始化时没有扫描到对应的 XML 文件。解决在 spring-mybatis.xml 中加入 mapperLocations 配置显式指定 XML 文件位置。这个坑只会在 Maven 结构中遇到因为 Maven 默认不会把 src/main/java 下的 XML 文件打包到 classes 目录如果 Mapper 接口和 XML 放在同一个包下就会导致运行时找不到 XML。bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource / property namemapperLocations valueclasspath:mybatis/*.xml / /bean6. 二次开发指南把二维码签到升级为带照片验证的签到系统这个项目预留了很多可以扩展的切入点。这里分享一个实际改造案例在原有扫码签到基础上增加拍照验证的功能。改造前思考一下二维码签到最大的问题是学生可以截屏签到码发给同学代签。增加拍照验证后学生扫码时需要拍摄一张现场照片服务端把照片和已存储的该学生入学照片做比对相似度超过阈值才允许签到。技术选型上客户端需要增加一个拍照模块。Android 原生提供 Intent 调起相机拿到照片后压缩上传到服务端。服务端的人脸比对可以使用开源库常见的方案是两个一个是调用第三方云服务的人脸比对接口另一个是本地做人脸特征提取和余弦相似度计算。接口设计层面在 SignController.java 中新增一个接口。PostMapping(/signWithPhoto) ResponseBody public Result signWithPhoto(RequestParam(signCode) String signCode, RequestParam(studentId) Integer studentId, RequestParam(file) MultipartFile file, HttpServletRequest request) { // 1. 保存照片到本地磁盘或OSS String photoUrl uploadFile(file); // 2. 调用人脸比对服务 boolean verified faceCompareService.compare(photoUrl, getStudentPhotoUrl(studentId)); if (!verified) { return Result.error(人脸校验未通过); } // 3. 执行原有签到逻辑 return sign(signCode, studentId); }这个改造步骤的关键在于保持原有 sign 方法的复用性。签到逻辑没有改变只是在签到的前置链路中增加了人脸校验这一步。这样原有测试用例仍然可以复用新增代码的风险范围被控制在新增链路内相当于给系统加了一个可插拔的校验器。改造完成后实际验证时用了一个例子A同学用手机扫描二维码拍摄了自己的照片签到成功然后让 B 同学拿着手机扫描同一个二维码拍摄的却是 A 同学的照片人脸比对失败签到被拒绝。这个验证方式确认了改造方案的可行性。从那以后我每次做类似功能改造都强制走一遍第三方服务依赖注入、原有逻辑保持兼容、失败场景模拟这三步检查流程。希望这个项目的解析和改造思路能帮到你让你在跑通这个签到系统的路上少踩几个坑。本文还有配套的精品资源点击获取