ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于SSM框架的微信小程序课堂考勤系统设计与实现解析

基于SSM框架的微信小程序课堂考勤系统设计与实现解析 简介课堂考勤微信小程序的毕业设计完整交付包基于微信小程序SSMMySql搭建涵盖小程序客户端、Web教师端和管理端三个身份体系。学生端可实现课程推荐、查询、详情签到、收藏与评论并支持人脸信息校验教师端可发布课程、管理评论、提交签到与出勤信息管理员后台则覆盖学生与教师管理、课程分类、签到命令、出勤报表统计及轮播图维护等核心模块。整个压缩包共923个文件、39.9MB融合png/svg静态资源、js/vue/wxml/wxss前端代码、java后台逻辑、SQL数据库脚本、Word毕业论文与mp4视频演示还附带了bat部署脚本和多种配置备份文件便于本地快速启动和二次开发。该资源已有182人学习适合计算机相关专业学生作为毕业设计参考既可运行演示、阅读源码也能对照论文梳理系统设计思路。1. 课堂考勤项目的角色边界与技术选型做过教务类系统的人都知道课堂考勤最难的地方从来不是“签到”这个动作本身而是“谁有权限发起签到、谁有权限改出勤结果、学生迟到缺勤的数据最终归到哪个报表里”。这届毕业设计选题里基于SSM框架的微信小程序考勤系统之所以适合拿来拆解核心就在于它把学生、教师、管理员三个身份从数据库字段到业务逻辑彻底分开学生看到的是课堂与签到教师管理的是课程与出勤率管理员则纵览全局统计数据。这套项目使用微信小程序作为学生客户端后端采用Java的SSM框架Spring SpringMVC MyBatis数据落在MySQL。优点是前后端职责清晰只要接口字段约定好前端切到更高的uni-app技术栈也不影响后端。本篇文章会选取几个最值得关注的模块数据库设计、签到核心逻辑、微信小程序端API协作方式、以及源码部署时的排查路径。内容按一个工程师拆代码的思路写适合直接拿来对照阅读。2. 考勤系统的数据库建模与出勤状态机2.1 三端用户模型的拆分方式用户模型拆成三张主表是这类项目的常见做法。t_student与t_teacher与t_admin三张表分开存而不是统一塞进一张t_user这在面试答辩时是加分项。因为学生有学号、班级、专业、入学年份教师有工号、学院、职称管理员只是后台维护账号三方字段差异较大合表会出现大量冗余空字段。三张表通过一个统一的username字段和password字段对应登录入口。密码字段在数据库中建议直接存md5后的密文而不是明文。代码实现里登录接口拿到页面传过来的明文密码后做一次MD5加密再查库这样即使数据库被导出也不会大面积泄露原始账号密码。t_student表结构可以参考以下SQL片段。CREATE TABLE t_student ( id int(11) NOT NULL AUTO_INCREMENT, stu_no varchar(20) DEFAULT NULL COMMENT 学号, stu_name varchar(50) DEFAULT NULL COMMENT 姓名, class_name varchar(50) DEFAULT NULL COMMENT 班级, major varchar(50) DEFAULT NULL COMMENT 专业, phone varchar(11) DEFAULT NULL, password varchar(64) DEFAULT NULL COMMENT MD5密文, face_path varchar(255) DEFAULT NULL COMMENT 人脸图片路径, PRIMARY KEY (id), UNIQUE KEY uk_stu_no (stu_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;stu_no加唯一索引用于防止学生重复注册。face_path字段用于人脸校验场景本项目中学生学习通账号会绑定人脸照片这里记录的是服务器上存储的图片相对路径。通过stu_no登录而不是id是因为学号对学生来说是稳定业务标识而自增id在数据迁移与合并时可能变化。2.2 课程与签到命令的组合关系课程表t_course负责维护教师发布的课程信息包含课程名称、课程编号、授课教师、上课时间、上课地点等。这个表与t_teacher表通过teacher_id关联和学生没有直接关系因为学生端“推荐课程”的数据来源是另一张选课关系表。关键点在签到表t_signin它的设计直接决定了出勤统计的准确性。这张表需要同时记录谁发起签到、签到对应的课程、签到时间语义、学生签到的状态。连续签到与补签逻辑必须在后端的Controller层做好控制不能只靠数据库约束。以下给出一个精简的建表语句。CREATE TABLE t_signin ( id int(11) NOT NULL AUTO_INCREMENT, course_id int(11) DEFAULT NULL COMMENT 课程id, student_id int(11) DEFAULT NULL COMMENT 学生id, teacher_id int(11) DEFAULT NULL COMMENT 教师id, sign_time datetime DEFAULT NULL COMMENT 签到时间, status tinyint(4) DEFAULT 1 COMMENT 1正常 2迟到 3缺勤 4请假, command_code varchar(10) DEFAULT NULL COMMENT 签到暗号, PRIMARY KEY (id), KEY idx_student_course (student_id,course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;status字段在代码中通过常量类统一维护业务层不要直接写数字。command_code是教师端生成的签到暗号学生端需要输入正确的暗号才能签到成功用于限制学生“离开教室也能签到”的问题。统计出勤率时以course_id status为维度做聚合查询。2.3 出勤统计报表的聚合思路管理员端“查看课程签到统计报表”和“出勤情况管理及报表查询”两个功能本质都是对t_signin表做多条件聚合。常见的做法是按课程分组统计总人数、应到人数、实到人数、缺勤人数。以下查询可以统计每门课程的签到情况。SELECT c.course_name, COUNT(DISTINCT s.id) AS total_students, SUM(CASE WHEN si.status 1 THEN 1 ELSE 0 END) AS normal_count, SUM(CASE WHEN si.status 2 THEN 1 ELSE 0 END) AS late_count, SUM(CASE WHEN si.status 3 THEN 1 ELSE 0 END) AS absent_count FROM t_course c LEFT JOIN t_signin si ON c.id si.course_id LEFT JOIN t_student s ON si.student_id s.id GROUP BY c.id, c.course_name;报表模块千万不能只给这一个SQL就完事。毕业设计答辩时老师通常会追问“如果有学生一次都没签到LEFT JOIN会不会把他丢掉”。此处的left join关联t_student时是以签到表中出现过的学生为基准真正从未签到的学生并不在t_signin表中所以学号、姓名需要通过t_student表单独维度补全。更稳妥的表结构设计是维护一张选课关联表中间表把学生与课程的从属关系持久化避免统计口径出现偏差。3. SSM后端分层实现与签到核心逻辑3.1 Controller层接口设计与参数校验SSM项目里Controller层主要负责接收参数、调用Service、包装返回结果。课堂考勤小程序的后端接口需要遵循“一个页面一个接口”的粗粒度设计减少小程序端频繁请求。例如学生端课程详情页一次请求就返回课程信息、教师信息、当前用户是否已收藏、该课程的评论列表合并数据而不是拆成4个接口让前端自己拼装。下面是一个典型的Controller方法功能是学生端提交签到。Controller RequestMapping(/api/signin) public class SigninController { Autowired private ISigninService signinService; ResponseBody RequestMapping(value /doSignin, method RequestMethod.POST) public Result doSignin(RequestBody SigninRequest request) { // 参数校验 if (request.getStudentId() null || request.getCourseId() null) { return Result.fail(学生ID和课程ID不能为空); } if (StringUtils.isEmpty(request.getCommandCode())) { return Result.fail(签到暗号不能为空); } // 签到核心服务 SigninResult result signinService.processSignin(request); return Result.success(result); } }Result是一个统一返回包装类里面包含code、msg、data三个字段。这里使用RequestBody接收JSON对象是因为微信小程序端通过wx.request提交数据时默认content-type是application/json。如果使用传统的表单提交参数接收方式需要改成RequestParam两者容易混淆调试时要先看请求头里的content-type。3.2 Service层签到状态机的流转控制签到不是简单在t_signin表里插一条数据就行。要考虑学生是否重复签到、当前时间是否在教师规定的签到时间段内、签到暗号是否正确、学生是否选了这门课。四个条件任何一个不过都不能写入成功记录。在Service层中处理这些逻辑判断顺序是关键。先查暗号再查选课关系之后校验时间窗口最后做防重复插入。顺序乱了数据量小时看不出问题但会在日志排查时增加难度。下面给出核心代码片段。public SigninResult processSignin(SigninRequest request) { // 1. 根据教师ID和课程ID查询当前签到命令 SigninCommand command signinCommandMapper.selectValidCommand( request.getCourseId()); if (command null) { return SigninResult.fail(当前没有有效的签到指令); } if (!command.getCommandCode().equals(request.getCommandCode())) { return SigninResult.fail(签到暗号错误); } // 2. 校验选课关系 Integer count studentCourseMapper.checkExist( request.getStudentId(), request.getCourseId()); if (count null || count 0) { return SigninResult.fail(您未选择该课程); } // 3. 校验时间 Date now new Date(); if (now.before(command.getStartTime()) || now.after(command.getEndTime())) { return SigninResult.fail(当前不在签到时间范围内); } // 4. 防重复签到 Signin exist signinMapper.selectByStudentAndCourseAndDate( request.getStudentId(), request.getCourseId(), command.getId()); if (exist ! null) { return SigninResult.fail(您已签到请勿重复提交); } // 5. 插入签到记录时间超过10分钟标记为迟到 Signin signin new Signin(); signin.setCourseId(request.getCourseId()); signin.setStudentId(request.getStudentId()); signin.setTeacherId(command.getTeacherId()); signin.setSignTime(now); // 判断迟到 long diff now.getTime() - command.getStartTime().getTime(); if (diff 10 * 60 * 1000) { signin.setStatus(2); } else { signin.setStatus(1); } signinMapper.insert(signin); return SigninResult.success(); }注意签到时间范围指的是教师发起签到时设定的起止时间不是课程的开始结束时间。很多初次接触这个项目的开发者会把这两个时间混为一谈导致学生在课程开始前就能签到。实际项目中教师发起签到的动作本身就携带了时间窗口课程表里的上课时间只是展示用。3.3 MyBatis Mapper层SQL与动态参数SSM中MyBatis承担数据访问职责。Mapper.xml里通过where标签自动拼装动态查询条件是面试时考察基本功的常客。以管理员端的学生信息管理为例需要支持按姓名模糊查询、按班级精确查询、按学号精确查询三种组合条件。select idselectStudentList resultTypemap parameterTypemap SELECT id, stu_no, stu_name, class_name, major, phone FROM t_student where if teststuName ! null and stuName ! AND stu_name LIKE CONCAT(%, #{stuName}, %) /if if testclassName ! null and className ! AND class_name #{className} /if if teststuNo ! null and stuNo ! AND stu_no #{stuNo} /if /where ORDER BY stu_no /select这里resultType写成map是为了让字段名直接映射到前端表格的列名省去再建一个VO类的时间。如果是正式团队项目通常不赞成这种写法因为map无法体现类型信息后期维护困难。毕业设计场景下用map可以快速看到效果但要能说清楚map写法的局限性。4. 微信小程序端与后端API的协作实现4.1 登录流程与openid的获取处理微信小程序登录与Web端登录的最大区别在于code2Session这个环节。小程序端调用wx.login拿到临时code后端拿这个code去微信接口换取用户唯一标识openid和会话密钥session_key。随后后端返回一个自定义token小程序把token存到storage中后续请求都在header里携带。核心技术点在于SSM后端要新增一个接口用于接收前端传来的code。很多毕业设计项目直接让前端把code作为一个普通参数传给登录接口这种做法在单体系统中完全可行。ResponseBody RequestMapping(value /login, method RequestMethod.POST) public Result wxLogin(RequestBody WxLoginRequest request) { // 1. 使用code调用微信接口换取openid String url https://api.weixin.qq.com/sns/jscode2session?appid appid secret secret js_code request.getCode() grant_typeauthorization_code; String result httpClientUtils.doGet(url); JSONObject json JSONObject.parseObject(result); String openid json.getString(openid); if (StringUtils.isEmpty(openid)) { return Result.fail(微信登录失败); } // 2. 判断openid在student表中是否存在 Student student studentMapper.selectByOpenid(openid); if (student null) { return Result.fail(该微信未绑定学生账号); } // 3. 生成token返回 String token UUID.randomUUID().toString().replaceAll(-, ); redisUtil.set(token, student.getId(), 24 * 60 * 60); return Result.success(token); }需要注意的是jscode2session接口的appid和secret不能硬编码在前端代码里。微信官方要求这两个参数只保存在后端服务中。如果在审核过程中发现小程序的代码包里出现真实appsecret审核可能被驳回。本文示例代码中使用了redisUtil存储token如果本地环境没有安装Redis可以临时改成ConcurrentHashMap缓存但重启后token失效属于临时方案。4.2 课程列表与签到按钮的联动页面逻辑小程序端课程列表页面使用wx:for渲染课程数据列表项的“签到”按钮通过>Page({ data: { remainSeconds: 0, expired: false, courseId: null }, startCountdown: function (endTime) { const that this; const timer setInterval(function () { const now Date.parse(new Date()) / 1000; const end Date.parse(endTime) / 1000; const remain end - now; if (remain 0) { that.setData({ remainSeconds: 0, expired: true }); clearInterval(timer); } else { that.setData({ remainSeconds: remain }); } }, 1000); }, handleSigninTap: function (e) { if (this.data.expired) { wx.showToast({ title: 签到已结束, icon: none }); return; } // 弹出暗号输入框 wx.showModal({ title: 请输入签到暗号, editable: true, success(res) { if (res.confirm res.content) { that.submitSignin(that.data.courseId, res.content); } } }); } });代码中通过editable: true让wx.showModal变成带输入框的对话框比自己写自定义弹窗省事很多。res.content就是用户输入的暗号内容。倒计时使用Date.parse转换为秒级时间戳做差值计算比直接用Date对象相减更直观而且兼容iOS系统对日期格式的解析区别。4.3 人脸信息校验的图像处理方案摘要中多次提及“人脸信息校验”功能这在小程序端不是一个复杂模型训练任务而是一个图像比对流程。学生在个人中心上传人脸照片时后端保存图片后调用人脸比对接口与预存的照片做相似度计算。常见的实现套路有两种一种是调用第三方开放平台的API另一种是使用Java的OpenCV库计算特征值。如果采用百度人脸识别或腾讯云人脸识别后端代码只需要上传图片到对应接口拿到返回的score值超过80分判定为同一个人。这种方式实现成本最低、效果稳定但需要注意接口的免费调用量限制。毕业设计演示阶段通常不会触发限制。public boolean verifyFace(String uploadPath, String storedPath) { FaceMatchRequest request new FaceMatchRequest(); request.setImageType(BASE64); request.setImageBase64(ImageUtils.imageToBase64(uploadPath)); request.setStoredImagePath(storedPath); FaceMatchResponse response faceClient.match(request); if (response.getScore() 80) { return true; } return false; }这里的FaceMatchRequest与FaceMatchResponse是SDK自带的请求响应对象不是SSM框架里的Model。如果开发时引用了人脸识别的SDK通常意味着依赖体积会变大Maven构建时首次下载依赖较慢。给项目打包时需要注意这些第三方jar包是否被正确打进war文件。5. 数据库初始化、端侧打包与常见排错5.1 本地环境快速启动项目的完整顺序源码包拿到手之后建议按照“数据库 → 后端 → 小程序端 → Web管理端”的顺序执行不要一上来就打开小程序开发工具。实际踩坑时反过来操作的代价非常高前端配置了域名却发现后端没启动后端启动成功却连接不上数据库层层排查浪费时间。首先用Navicat或命令行工具执行项目目录下的init.sql脚本初始化数据库。然后修改jdbc.properties文件中的数据源配置重点检查useSSLfalse的参数避免在高版本MySQL连接时出现SSL握手警告。配置文件中以下参数是必须关注的。jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/class_checkin?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456serverTimezone不设置会报时区错误这是MySQL 8.0以上版本中的高频问题。characterEncodingutf8保证中文字符正常存取useSSLfalse能跳过证书验证环节。后端使用Eclipse导入maven工程后右键项目选择Run As下的Maven Clean然后再执行Maven Install。等待依赖下载完成后在3-build.bat中能看到打包成war包后自动部署到Tomcat的webapps目录。双击2-run.bat启动Tomcat日志中出现Server startup in关键字说明后端已启动。5.2 小程序端域名白名单与request请求失败排查小程序开发环境中勾选“不校验合法域名”通常可以正常调试但真机预览时会遇到request:fail url not in domain list的报错。这个提示不是代码bug而是微信平台的域名备案校验机制。后端接口必须部署到HTTPS域名下并在小程序后台的“开发管理-服务器域名”中把域名加入request合法域名。源码包中前端请求工具类通常统一封装在utils/request.js查看代码时关注baseUrl变量。const BASE_URL https://yourdomain.com/class_checkin; function request(url, method, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL url, method: method, data: data, header: { Content-Type: application/json, token: wx.getStorageSync(token) }, success: function (res) { if (res.statusCode 200) { resolve(res.data); } else { reject(res); } }, fail: function (err) { reject(err); } }); }); }开发阶段可以把BASE_URL改成http://localhost:8080/class_checkin但要用“不校验合法域名”模式。线上演示时必须改成HTTPS的正式域名。header中的token从storage取出如果token过期后端会返回统一错误码前端需要做一个响应拦截并跳转登录页。5.3 教师端以“修改刚进入的加载页面”为切入点的权限控制Web端教师管理系统的前端基于Vue与ElementUI开发自带的登录逻辑与后端接口对接。源码包中出现了IndexMain.vue.bak、IndexHeader.vue.bak等备份文件这些.bak后缀文件通常用来标记修改前的版本。教师登录后读取的是t_teacher表的数据而不是t_student表如果数据库中不存在该教师的工号记录登录就会失败。排查这类问题时先确认数据库的教师表中是否存在对应账号再确认页面中调用的登录接口路径与后端Controller的RequestMapping值保持一致。常见错误是项目在迁移到新环境后部署路径变化导致后端接口URL前缀不一致。权限过滤可以在Vue的路由守卫中实现也可以在后端增加一个HandlerInterceptor拦截器。推荐使用拦截器方案因为前端路由守卫只能阻止页面跳转不能阻止直接发起AJAX请求访问接口。public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(token); if (StringUtils.isEmpty(token)) { response.setStatus(401); return false; } Object userId redisUtil.get(token); if (userId null) { response.setStatus(401); return false; } return true; } }在spring-mvc.xml中配置这个拦截器并指定拦截路径为/api/**。值得注意的是/api/login接口本身不能被拦截否则登录逻辑会陷入死循环。白名单配置在毕业设计答辩中属于加分项能体现出开发者对认证流程的完整性思考。5.4 源码替换与二次开发的最佳修改入口如果要在本项目基础上做二次开发建议从“签到命令信息管理”这个模块入手。因为它的数据流最短管理员配置命令 → 教师发起签到 → 学生输入命令签到 → 统计报表查询。修改这一条链路的相关性最强对比效果明显。切换技术栈时比如将SSM框架替换成SpringBoot体系工作量主要集中在依赖管理和配置类中。原本web.xml中的Servlet配置在SpringBoot里变成了自动配置但MyBatis的Mapper接口和XML文件不需要大改因为两者都是直接基于JDBC封装映射逻辑独立于框架。把项目压缩包中的数据库目录导出为SQL文件时注意字符集选择推荐使用utf8mb4否则插入emoji表情符号时会报Incorrect string value错误。每次修改完数据库结构后在后端Mapper层新增的字段名要与数据库列名保持一致避免MyBatis的mapUnderscoreToCamelCase开启状态导致字段不匹配的坑。本文还有配套的精品资源点击获取
返回列表