首页 / 资讯中心 / 文章详情

基于安卓与SSM框架的学生签到系统设计与实现

基于安卓与SSM框架的学生签到系统设计与实现 ★ FEATURED ARTICLE
简介基于安卓平台的 SSM 架构学生签到系统面向高校课程考勤与课堂管理场景适合 Android 开发学习者、毕业设计选题学生以及需要快速搭建签到功能的后端开发者。系统实现扫码签到、数字签到与定位签到三种模式支持签到记录的修改调整并附带加课码选课功能覆盖课堂考勤与选课管理的常见业务闭环。资源包共 3 个文件包含前端 Android Studio 工程、后端 IDEA 工程以及初始化数据库 SQL 文件整体压缩包约 139MB。后端采用 SSM 框架搭建前端为原生 Android 项目数据库脚本内含初始数据导入后即可直接运行调试项目部署到服务器的流程可在配套博客中查阅。目前已有 838 人学习下载对于希望完整跑通移动端考勤系统前后端联调、理解 SSM 与 Android 交互流程的开发者这份资源提供了可直接参考的项目结构和可运行代码。1. 从老师抱着一摞点名册的崩溃现场说起签到这件事说起来简单做起来却一直是校园里的老大难。我见过某高校一位老师一节课五十多个学生挨个喊名字要花五分钟底下还有帮忙答“到”的老师气得把点名册翻得哗哗响。后来我就想能不能做一个让老师手机上点一点就能看到谁来了、谁没来学生拿着自己手机就能完成签到的系统于是就有了这个基于安卓的学生签到系统。它不是一个简单的打卡App而是一套完整的“安卓客户端 SSM后端”的解决方案学生端用Android Studio开发老师端和管理端通过Web页面操作后端用标准的Spring SpringMVC MyBatis也就是常说的SSM提供接口。适合谁适合正在做毕业设计的学生也适合想练手安卓与后端联调的全栈开发者。这套代码把登录、课程管理、签到、统计这些环节全部串通了拿来就能跑跑起来就能体会到一个真实项目从数据库到手机界面的完整链路。2. 系统架构拆解安卓端和SSM后端是怎么分工的2.1 三层结构表现层、业务层、数据层这个项目最让我满意的地方在于它的分层非常规矩没有乱糟糟把代码堆在一起。按照SSM的标准套路后端分了controller、service、mapper三层。Controller负责接收安卓端发来的HTTP请求解析参数Service层写业务逻辑比如判断签到时间是否有效、是否重复签到Mapper层就是MyBatis的接口直接跟数据库打交道。安卓端这边也不是一股脑全写在Activity里。项目里把网络请求封装成了一个独立的包用OkHttp或HttpURLConnection发送POST/GET请求再通过Gson解析JSON。这样做的好处是以后如果想把后端改成Spring Boot安卓端几乎不用动只要接口路径不变。数据库表设计上项目至少包含这几个核心表student学生表、teacher教师表、course课程表、sign_record签到记录表。sign_record表是核心它记录了学生id、课程id、签到时间、签到状态正常、迟到、缺勤以及签到方式手动签到或基于WiFi定位。这些字段设计得挺全直接支撑了后面的统计功能。2.2 安卓端的模块划分登录、课程列表、签到、统计安卓端的主界面是一个典型的底部导航栏结构分成几个Tab首页显示今日课程消息页占位个人中心。每个Tab对应一组Activity和Fragment。登录模块支持学生和教师两种角色登录后通过intent跳转到不同的主界面。课程列表模块用得比较多的是RecyclerView每个item展示课程名称、上课时间、任课教师。点击课程后如果是学生角色进入签到页如果是教师角色进入该课程的签到管理页。签到页做的比较细致有“开始签到”和“一键签到”两个逻辑教师发起签到后学生端会收到一个时间窗口只有在这个窗口内签到才算有效。统计模块是给教师用的可以按课程维度查看某一次签到中多少学生正常、多少迟到、多少缺勤。这个统计并不复杂就是后端从sign_record表里按courseId和时间查询然后在Service里做个分组。2.3 网络接口约定安卓端和后端的对接桥梁后端给安卓端暴露的接口路径是写死了的比如POST /user/login参数username、password、role返回用户基本信息GET /course/list?studentIdxxx返回某学生的课程列表POST /sign/start教师发起签到参数courseId、teacherIdPOST /sign/submit学生提交签到参数courseId、studentId、signTimeGET /sign/record?courseIdxxx教师查询签到记录这些接口的返回格式统一是JSON结构为code200表示成功500表示失败、message提示信息、data具体数据。安卓端在解析时会先判断code再取出data部分。这样做的好处是后端的异常可以在Service层捕获并转成JSON返回安卓端就不用担心收到一堆堆栈信息。3. 核心代码逐段拆解一个签到请求从点击到入库的完整链路3.1 安卓端如何封装网络请求和解析JSON我一般会在安卓端写一个HttpUtil工具类把网络请求统一封装起来。这个类用的是OkHttp的同步模式再开子线程去调用避免在主线程做网络操作。下面这段是登录请求的关键代码// HttpUtil.java public class HttpUtil { // 后端服务器地址模拟器访问本机要用10.0.2.2 public static final String BASE_URL http://10.0.2.2:8080/ssm_sign/; public static String login(String username, String password, String role) { String url BASE_URL user/login; // 用OkHttp的FormBody构建POST参数 FormBody body new FormBody.Builder() .add(username, username) .add(password, password) .add(role, role) .build(); Request request new Request.Builder() .url(url) .post(body) .build(); try { Response response new OkHttpClient().execute(request); return response.body().string(); } catch (IOException e) { e.printStackTrace(); return null; } } }这段代码有个关键点BASE_URL里的10.0.2.2是Android模拟器访问宿主机器的特殊地址如果你用的是真机调试就要把这里改成电脑在局域网里的IP比如192.168.1.100否则必坑。我在第一次联调时就是因为这个地址没改浪费了半天时间。另外OkHttp的execute()是同步方法必须放在子线程里跑调用的时候要配合Java的Thread或者AsyncTask否则Android会直接抛NetworkOnMainThreadException。拿到JSON字符串以后用Gson解析成实体对象。项目里有一个UserBean包含userId、username、role等字段。登录成功后就把这个bean存到SharedPreferences里App全局都能用。3.2 后端Controller接收参数、校验身份、转发给Service后端以Spring MVC为主每个Controller都是通过Controller和ResponseBody注解返回JSON。我们看登录这个接口的实现// UserController.java Controller RequestMapping(/user) public class UserController { Autowired private UserService userService; RequestMapping(value /login, method RequestMethod.POST) ResponseBody public MapString, Object login(String username, String password, String role) { MapString, Object result new HashMap(); User user userService.login(username, password, role); if (user ! null) { result.put(code, 200); result.put(message, 登录成功); result.put(data, user); } else { result.put(code, 500); result.put(message, 用户名或密码错误); } return result; } }这段代码没什么玄学但有几个细节需要注意。ResponseBody会把返回的Map自动转成JSON这是Spring MVC内置的Jackson处理的所以不需要自己手动拼JSON字符。方法参数直接写String usernameSpring会从POST请求体里自动取值比用RequestParam注解少写一行。Service层的login方法返回的是User实体如果查不到就返回null。这里我习惯的做法是返回值统一用MapString, Object代替自定义的Result类。虽然这样不够“面向对象”但胜在简单直接接口的返回结构一目了然读者拿到代码也好理解。等以后要扩展成大型项目再去封装统一的ResponseEntity也不迟。3.3 签到核心逻辑怎么防止重复签到和判断时间窗口签到是系统的核心也是最容易出bug的地方。后端Service在这里做了两件事一是判断当前时间是否在签到时间窗口内二是查数据库看这个学生是否已经签到过。// SignService.java public boolean submitSign(int courseId, int studentId, String signTime) { // 签到时间窗口假设教师发起签到后30分钟内有效 Date now new Date(); SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd HH:mm:ss); try { Date signDate sdf.parse(signTime); if (now.getTime() - signDate.getTime() 30 * 60 * 1000) { return false; // 超出窗口拒绝 } } catch (ParseException e) { e.printStackTrace(); return false; } // 查该学生在该课程下是否已有签到记录 SignRecord record signRecordMapper.findByCourseIdAndStudentId(courseId, studentId); if (record ! null) { return false; // 已经签过拒绝重复 } SignRecord newRecord new SignRecord(); newRecord.setCourseId(courseId); newRecord.setStudentId(studentId); newRecord.setSignTime(new Date()); newRecord.setStatus(1); // 1表示正常签到 return signRecordMapper.insert(newRecord) 0; }这里的时间判断逻辑值得多想一层。安卓端在点击签到按钮时会取当前时间传给后端后端拿这个时间和服务器当前时间比对。但如果你允许用户手动修改手机时间那这个防作弊就走了弯路。所以我在实际改造中更倾向让后端直接取new Date()以服务器时间为准安卓端传时间时只传一个“发起签到”的动作标识即可。这个项目里保留了传时间的写法是为了能让读者看到时间参数是怎么流转的但实际生产环境建议换掉。重复签到判断里用了findByCourseIdAndStudentId这个Mapper方法对应的SQL是在SignRecordMapper.xml里写的。MyBatis的XML文件里要特别注意SELECT语句的WHERE条件里多个字段之间用AND连接这个写法很基础但常有人写错成逗号。我在这个项目里就踩过查到后面发现是SQL写错了控制台报一个很隐晦的“Bad SQL grammar”错误。4. 从零跑通这个项目环境配置、数据库导入、安卓联调4.1 开发环境准备清单这个项目用了两套IDE后端用IntelliJ IDEA安卓用Android Studio。具体环境版本我建议这样配JDK1.8或以上这个项目用的是Java 8的语法太高的版本也不会报错Tomcat8.5或者9.0后端是打包成war包放到Tomcat里的MySQL5.7或8.0注意8.0的驱动要换Android Studio建议用Hedgehog或之后的版本Gradle配置兼容性会更好后端框架Spring 5.x、SpringMVC 5.x、MyBatis 3.5.x如果你用的是IntelliJ IDEA直接打开后端工程配置好Tomcat后点击运行按钮就能把SSM项目启动起来。这一步的常见翻车点是用户没有把项目设置成Maven工程导致IDEA里找不到src/main/java目录。解决办法是右键项目根目录选择“Add as Maven Project”。4.2 数据库初始化创建表并插入测试数据数据库的初始化脚本在项目的doc/sign_db.sql文件里用Navicat或者命令行执行都可以。核心表就四张我用SQL把最重要的sign_record表展示出来CREATE TABLE sign_record ( id int(11) NOT NULL AUTO_INCREMENT, course_id int(11) DEFAULT NULL COMMENT 课程ID, student_id int(11) DEFAULT NULL COMMENT 学生ID, sign_time datetime DEFAULT NULL COMMENT 签到时间, status int(1) DEFAULT NULL COMMENT 签到状态1正常2迟到3缺勤, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8;注意表名和字段名如果有sign这样的词在MySQL里不是保留字所以不会冲突。但如果你要用order或者group做字段名那就必须加反引号了这是个常见的坑。插入测试数据时要保证student表里的student_id和course表里的course_id是真实存在的因为在service层我们直接用了这个外键关系去查记录并没有加上复杂的外键约束。这种做法在demo项目里很常见跑起来没问题但如果后续要做严格的数据校验还是建议加上外键或至少加索引。4.3 安卓端配置服务器地址和打包安装安卓端拿到手以后第一件事是确认HttpUtil.java里的BASE_URL。如果你用的是Android Studio自带的模拟器地址保持http://10.0.2.2:8080/ssm_sign/不变。注意这里有一个路径“/ssm_sign”它对应的是你在Tomcat里部署war包时的上下文路径。如果你部署时把war包名字改成别的比如sign_system.war那这里的路径也要跟着改否则会404。如果你用的是真机那BASE_URL要改成电脑的局域网IP改完后务必确认手机和电脑连的是同一个WiFi。用命令ipconfigWindows或者ifconfigMac/Linux查看电脑IP然后把这个IP写进去。还有Windows防火墙会拦截Tomcat的8080端口这也是个让人头大的问题解决方法是去“Windows Defender防火墙”里添加入站规则允许8080端口通过。我把这个问题写进避坑章节里就是因为至少有一半的联调失败是卡在这。4.4 联调验证从登录到签到的全流程测试跑通的验证方法很简单先启动Tomcat确保后端不报错。然后在数据库里插入一条测试数据一个老师、一个学生、一门课。用安卓客户端登录学生账号成功后会看到课程列表点击“签到”如果当前时间在教师发起签到的30分钟内就会提示“签到成功”。这时去数据库的sign_record表查能看到多了一条记录再去教师端Web页面查看统计能看到该学生状态为“正常”。我建议你用Postman直接测后端接口再跑安卓端这样出了问题能快速定位。比如先POST/user/login返回code:200说明后端活着如果Postman都返回500那就去IDEA的控制台看异常堆栈八成是MyBatis映射文件没扫到或者数据库连接配置错了。5. 避坑指南这个项目最常见的五个翻车点5.1 后端启动时报“Invalid bound statement (not found)”现象Tomcat启动时报这个错文件明明存在但Mapper就是找不到。原因MyBatis的Mapper接口和XML文件没有放在同一个包路径下。很多项目会习惯性把XML放到resources目录但配置里没指定mapper-locations。解决在Spring的配置文件spring-mybatis.xml里加上property namemapperLocations valueclasspath:mapper/*.xml/同时确保java接口和服务层里MapperScan或者Mapper注解的包路径是正确的。我见过有人以为这个错是代码问题折腾了一天发现就是少了一行xml配置。5.2 安卓端网络请求一直超时或连接不上现象点击登录后等了很久才弹“网络连接失败”或者干脆一直转圈。原因八成是BASE_URL地址写错了。模拟器和真机的地址规则不同前面已经说了。还有一个很隐蔽的问题——项目里安卓Manifest没有加INTERNET权限。虽然Android Studio新建项目时默认会带吗实际上并不会你得手动在AndroidManifest.xml里加一句uses-permission android:nameandroid.permission.INTERNET/否则网络请求直接被系统拦截。解决先检查权限再用浏览器或Postman去访问你的后端地址看通不通。如果电脑从局域网访问不通先关掉防火墙试试。5.3 签到接口返回500数据库报“主键重复”现象同一个学生重复签到正常应该提示“已签到”但却直接抛异常。原因Service层查重复签到用的findByCourseIdAndStudentId没有生效可能SQL条件写错了导致查出来的结果为null然后插入时又碰到主键冲突因为sign_record表里没加唯一约束但业务上不允许重复。解决在findByCourseIdAndStudentId的SQL里确保字段名和数据库列名一致。如果数据库列名是course_id那XML里要写course_id #{courseId}不能写成courseId。最好在表上加一个UNIQUE KEY从数据库层面强制防止重复这样即使代码逻辑漏了也不会脏数据。5.4 安卓端Gradle同步失败提示依赖下载不下来现象第一次同步项目时进度条走了一半就报错提示某个库或插件无法下载。原因因为国内网络访问Maven中央仓库不稳定或者你用了过高的Gradle版本与Android Studio内置版本不匹配。解决把项目的build.gradle里仓库源替换成国内镜像常用的是阿里云镜像maven { url https://maven.aliyun.com/repository/google } maven { url https://maven.aliyun.com/repository/public }然后再检查一下gradle-wrapper.properties里的distributionUrl版本改成Android Studio推荐的那个版本。我在重装Android Studio后遇到过一次就是版本对不上同步卡了半小时。5.5 后端接口返回的中文乱码现象安卓端收到的message是登录æˆ这样一堆乱码。原因Spring的ResponseBody默认使用了ISO-8859-1编码而代码里的中文是UTF-8。解决在SpringMVC配置里加上一个消息转换器或者简单粗暴地在每个Controller返回值前手动转码。最靠谱的方法是配置StringHttpMessageConverter把编码强制设为UTF-8。还有一个临时办法是返回JSON前调用result.put(message, new String(中文.getBytes(), UTF-8))但这治标不治本。最好还是在spring-mvc.xml里统一处理。6. 二次开发把签到系统改造成带GPS定位的版本如果你打算拿这个项目做毕业设计或者再接一个功能我建议把重点放在“防代签”上。目前这个签到系统其实没有防作弊学生只要拿到别人的手机或者让别人用他的账号登录就能代签。一个非常实用的改造就是加入GPS定位和WiFi白名单。改动并不复杂安卓端需要新增位置权限并获取经纬度。核心思路是在submitSign的请求参数里加上latitude和longitude后端在Service层判断该坐标是否在课程设置的签到范围内。具体做法是先给course表加两个字段lat和lng作为课程的教学楼坐标然后在签到逻辑里用Math.haversin计算两个坐标的距离超过100米就拒绝。写一段伪代码示意// 二次开发扩展距离判断 public boolean isInRange(double lat1, double lng1, double lat2, double lng2) { double R 6371000; // 地球半径单位米 double dLat Math.toRadians(lat2 - lat1); double dLng Math.toRadians(lng2 - lng1); double a Math.sin(dLat/2) * Math.sin(dLat/2) Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * Math.sin(dLng/2) * Math.sin(dLng/2); double c 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); return R * c 100; // 100米内 }这段代码里的球面余弦公式算出来的距离比较准我在实际项目里用过误差大概一两米对于教室签到足够用了。拿到这个公里数后再乘一个系数就能转成米。注意安卓端的定位服务最好用LocationManager.getLastKnownLocation因为FusedLocationProvider引入的依赖会加大项目体积demo里没必要。我还有一个习惯就是每一次改动都强制走一遍“用户登录-签到-查记录”的完整流程哪怕只是改了一个if条件。因为前后端联调时这种逻辑改动很容易影响接口的返回结构你不走完整流程就发现不了。还有一次我改完签到时间判断逻辑后用Postman测了接口是通的但安卓端一直提示“签到超时”查了半天发现是安卓端本地时间比服务器慢了三分钟。后来我就在每次开发前给模拟器强制同步一下时间从那以后很少再遇到这种玄学问题。希望这篇解析能帮你在跑这个项目时少踩几个坑祝早日跑通。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站