资讯动态

微信小程序学生签到系统:Java与SSM框架源码解析

发布时间:2026/10/18 14:06:17 来源:尧图企业网站定制
简介基于微信小程序与SSM框架的学生签到系统完整项目源码面向计算机专业毕业设计学生和Java/小程序开发者。系统覆盖用户管理、签到任务创建、一键及二维码签到、签到记录查询、教师管理、消息提醒、权限控制与数据统计分析等功能模块可满足学校日常考勤管理需求。压缩包共1087个文件约16MB包含Java后端代码、Vue管理端、微信小程序前端wxml/wxss/js、SQL初始化脚本及运行批处理脚本文件类型涵盖java、vue、js、png、xml等目录结构清晰便于部署与二次开发。目前已有132人学习下载。通过该项目可掌握微信小程序与SSM框架的整合方法、前后端数据交互流程及权限设计思路适合作为毕业设计选题或项目实战练手的参考资料。1. 基于微信小程序的学生签到系统Java 源码与 SSM 框架的落地形态做学生签到系统最容易翻车的不是功能写不出来而是微信小程序和 SSM 框架两条线对不上。这套基于微信小程序的学生签到系统后端是 SSMSpring SpringMVC MyBatis前端是原生微信小程序管理端是 Vue 页面是 Java 项目源码里很典型的毕业设计组合。它解决的核心问题是把教师点名、纸质签到表这种低效流程换成学生打开微信扫码或一键签到签到记录自动落库教师端按课程、按时间统计出勤率。适合正在挑毕业设计方向的人也适合想快速搭一套考勤 Demo 的从业者。资源里包含了后端、小程序端、Vue 管理端和三个一键脚本下面按我实际拆这套资源的顺序把结构、链路、部署和坑一次讲透。2. 拆开这套签到系统SSM 服务端、小程序端和三个启动脚本2.1 一个资源包两份代码这堆 .bak 文件是退路不是垃圾刚解压会看到一堆.bak后缀的文件main.css.bak、update-password.vue.bak、IndexMain.vue.bak、BreadCrumbs.vue.bak、IndexHeader.vue.bak别急着删。.bak是原开发者在改版时留下的备份比如IndexMain.vue.bak是管理端首页的旧版本update-password.vue.bak是修改密码页面的旧版本。这种命名习惯说明一点这套系统在迭代时保留了可回退的副本你改崩了还能翻回去。真正的运行代码分三块模块载体作用后端服务SSM 多模块 Maven 工程提供登录、签到、任务、统计等接口小程序端微信开发者工具工程学生登录、一键签到、扫码签到、查记录管理端Vue 单页应用教师创建任务、查看记录、导出数据文件列表里的1-install.bat、2-run.bat、3-build.bat是部署顺序的提示先装依赖再启动后端最后构建管理端前端。.vscode那几个文件不用管那是编辑器配置。2.2 SSM 服务端三层签到任务和记录怎么组织SSM 的核心是 Spring 管对象、SpringMVC 管接口路由、MyBatis 管 SQL 映射三层职责非常清楚Controller层只收参数和返回结果Service层写业务逻辑和事务Mapper层写 SQL。签到系统里最核心的两张业务表是sign_task签到任务和sign_record签到记录加上student、teacher就够撑起主流程。sign_task表结构大概是这样的字段类型说明idint主键teacher_idint创建任务的教师 IDcourse_namevarchar课程名称start_timedatetime签到开始时间end_timedatetime签到截止时间typetinyint1 一键签到2 二维码签到sign_record表记录每次签到行为核心字段是task_id、student_id、sign_time、status。status用 0 和 1 区分失败与成功迟到与否不直接存而是拿sign_time和start_time做差算出来这样统计口径可以随时调。Mapper 层用注解或 XML 都可以这个项目里两种写法混着用。XML 方式更利于复杂统计select idselectByTaskId resultTypeSignRecord SELECT id, task_id, student_id, sign_time, status FROM sign_record WHERE task_id #{taskId} /select逻辑上#{taskId}是预编译参数MyBatis 会把它转成?占位符能防 SQL 注入。如果你想按班级过滤加一行AND student_id IN (SELECT id FROM student WHERE class_name #{className})就行这就是整套统计功能的地基。2.3 小程序端与 Vue 管理端页面职责先分清小程序端典型的页面结构是pages/index/index做登录和首页pages/sign/sign做签到操作pages/record/record查历史记录。管理端的IndexMain.vue是主框架BreadCrumbs.vue是面包屑导航IndexHeader.vue是顶部栏IndexAsideStatic.vue是侧边菜单update-password.vue是改密页。这种组件拆分方式意味着管理端可以单独打包部署后端只需要提供 JSON 接口。小程序端调接口的公共请求层一般长这样// utils/request.js const BASE_URL http://localhost:8080/api function request(path, method, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method: method, data: data, header: { Content-Type: application/json }, success(res) { if (res.data.code 200) { resolve(res.data.data) } else { reject(res.data.msg) } }, fail: reject }) }) }BASE_URL这里写的是本地开发地址上线后要换成 HTTPS 域名。注意微信小程序生产环境要求接口域名必须备案并且配好 SSL 证书本地联调阶段可以勾选“不校验合法域名”跳过这层限制。Promise 包裹是为了让页面里用async/await写异步逻辑避免回调地狱。3. 微信登录与签到链路从 code 到 openid 再到签到记录3.1 微信登录不是“手机号密码”code 换 openid学生端登录不输账号密码而是调wx.login()拿一个临时code这个 code 只能用一次五分钟内有效。后端拿到 code 之后拿它去微信服务端换openid和session_key。openid是用户在这个小程序下的唯一 IDsession_key是会话密钥解密手机号等敏感信息时会用到。后端典型实现PostMapping(/wx-login) public Result wxLogin(RequestBody LoginRequest req) { String url https://api.weixin.qq.com/sns/jscode2session ?appid appId secret appSecret js_code req.getCode() grant_typeauthorization_code; String resp restTemplate.getForObject(url, String.class); JSONObject json JSON.parseObject(resp); String openid json.getString(openid); Student student studentMapper.findByOpenid(openid); if (student null) { student new Student(); student.setOpenid(openid); student.setName(微信用户 openid.substring(openid.length() - 4)); studentMapper.insert(student); } String token UUID.randomUUID().toString().replace(-, ); redis.set(token: token, student.getId().toString(), 7 * 24 * 3600); return Result.success(token); }这段做了三件事换 openid、查库、没有则自动注册然后发一个自定义 token 给小程序。appId和appSecret必须放后端放前端等于把钥匙挂在门口。restTemplate调用微信接口时返回里如果带errcode就不是成功响应必须判空处理。3.2 一键签到与二维码签到接口设计与时间窗限制一键签到的流程是教师端创建任务后小程序首页显示“当前进行中的签到任务”学生点一下按钮就调签到接口。二维码签到则是教师端生成一个包含taskId的二维码学生扫码自动跳转到签到页。签到接口核心逻辑PostMapping(/sign/doSign) public Result doSign(RequestBody SignRequest req) { SignTask task signTaskMapper.selectById(req.getTaskId()); if (task null) { return Result.error(签到任务不存在); } Date now new Date(); if (now.before(task.getStartTime()) || now.after(task.getEndTime())) { return Result.error(不在签到时间范围内); } SignRecord record new SignRecord(); record.setTaskId(task.getId()); record.setStudentId(req.getStudentId()); record.setSignTime(now); record.setStatus(1); signRecordMapper.insert(record); return Result.success(签到成功); }时间窗判断是防早签和补签的关键now.before(startTime)和now.after(endTime)两个条件缺一不可。如果你不想让学生反复签到sign_record表加一个task_id student_id的唯一索引数据库层面挡住重复提交。我就是在这个地方吃过亏刚开始只做了应用层判断学生连点两下就有两条记录。3.3 权限控制拦截器里做角色校验学生只能签到和查自己的记录教师能建任务、看统计这个用拦截器在 Web 层统一卡。SSM 项目的拦截器配置在 SpringMVC 的 XML 里或者用注解Interceptor注册。拦截器里校验逻辑public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(Authorization); if (token null || !redis.hasKey(token: token)) { response.setStatus(401); return false; } String role redis.get(role: token); if (/api/teacher/.startsWith(request.getRequestURI()) !teacher.equals(role)) { response.setStatus(403); return false; } return true; }放行规则要分清登录接口、签到接口放行管理端写操作接口必须验证 teacher 角色。request.getRequestURI()取到的是当前路径按前缀匹配角色比在每个 Controller 里写 if 判断要干净得多。4. 本地跑通部署三个 bat 脚本、IDEA 导入和开发者工具配置4.1 先把启动脚本的顺序搞对install 再 run最后 build资源里三个.bat文件已经给了顺序但它们不是双击就完事得先明白每个脚本做了什么。1-install.bat是 Maven 依赖安装2-run.bat是启动后端服务3-build.bat是构建 Vue 管理端。1-install.bat典型内容echo off cd /d %~dp0 call mvn clean install -DskipTests pause这里最值钱的是cd /d %~dp0。如果不加这行在资源管理器里双击 bat工作目录很可能落在C:\Windows\System32Maven 找不到项目的pom.xml直接报错退出。pause让窗口在命令结束后停住方便看日志。2-run.bat本质是启动内嵌 Tomcatecho off cd /d %~dp0 call mvn spring-boot:run如果项目不是 Spring Boot 而是传统 SSM 的 war 包那这行应该替换成 Tomcat 的startup.bat。拆的时候看一下pom.xml里有没有spring-boot-maven-plugin有就是 Boot 工程没有就得打 war 丢外置容器。3-build.bat构建管理端echo off cd /d %~dp0\admin call npm install call npm run build pausenpm install装的是 Vue 项目的依赖npm run build会执行vue-cli-service build产物输出到dist目录再用 Nginx 指过去就行。4.2 数据库与 SSM 配置改造MySQL 版本、时区、账号数据库先建好再改后端连接配置。SSM 的数据库配置一般在application.properties或jdbc.properties里spring.datasource.urljdbc:mysql://localhost:3306/sign_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai spring.datasource.usernameroot spring.datasource.password123456serverTimezoneAsia/Shanghai是血泪教训。MySQL 8.x 默认时区和中国本地时间差八小时不加这个参数签到时间全部少八小时统计迟到率全乱。characterEncodingutf8解决中文乱码useUnicodetrue是配套参数MySQL 5.7 和 8.0 都要写。IDEA 导入时注意三点选择Import Project然后选 Maven等待依赖下载完确认 JDK 版本和pom.xml里配置一致不一致会报invalid source releaseMaven 仓库路径尽可能用默认的.m2换路径后第一次构建会重新下载全网依赖非常折磨。数据库导入用 Navicat 或命令行都行mysql -u root -p sign_system.sql导入后重点检查三张表的数据量student里有没有测试账号sign_task里有没有残留任务。我做毕业设计时就是忘了清测试数据一跑起来全是别人班里的签到记录。4.3 小程序开发者工具联调配置小程序端导入项目时目录要选到包含app.json的那一层。AppID 可以先选测试号等真正上线再换成自己的。本地联调最关键的是把request.js里的BASE_URL改成自己电脑的局域网 IPconst BASE_URL http://192.168.31.24:8080/api用局域网 IP 而不是localhost是因为手机预览时手机访问的是你电脑的 IP不是手机自己的 localhost。开发者工具右上角“详情”里勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”本地 HTTP 接口才能调通。这步不做接口请求会直接报fail url not in domain list。后端启动时会占 8080 端口如果本机已经被其他程序占了改application.properties里的server.port8081小程序那边同步改。我曾经因为端口冲突排查了半天最后发现是杀毒软件占着端口换个端口世界清净。5. 避坑指南code2Session 到 Excel 导出的五个翻车点5.1 登录报 40029code 是一次性的烟火现象小程序登录经常失败后端日志出现errcode: 40029, errmsg: invalid code有时候白屏有时候弹“登录已过期”。原因wx.login()生成的 code 只能用一次且有效期只有五分钟。如果你在前端把 code 存在全局变量里多个请求复用同一个 code第二次调用必然失效。还有一种情况是页面onShow每次都调wx.login()上一次的 code 还没用完就被覆盖了导致并发请求拿到新的、旧的全部失效。解决前端每次需要登录态时重新调wx.login()不缓存 code后端拿到 code 后立即消费不做任何重试。如果后端调用微信接口超时要重试必须让前端重新生成 code不能拿旧 code 反复打。// 正确姿势每次登录重新拿 code wx.login({ success(res) { if (res.code) { request(/wx-login, POST, { code: res.code }) } } })5.2 签到成功但列表查不到Service 层没提交事务现象接口返回“签到成功”但sign_record表里没有数据刷新列表也看不到。原因Controller 里直接调 Mapper 插入数据没有走Transactional或者 Service 方法内部抛了异常但事务没回滚。最常见的是 Service 里先insert再更新任务计数第二步抛了空指针事务默认不会自动回滚但数据也没落到库里表现就是“接口成功、库里空空”。解决Service 方法加Transactional(rollbackFor Exception.class)让所有运行时异常都触发回滚。插入和更新放一个事务里要么都成功要么都失败Transactional(rollbackFor Exception.class) public void doSign(SignRequest req) { signRecordMapper.insert(record); signTaskMapper.increaseSignCount(req.getTaskId()); }从那以后我接手这类 SSM 项目先翻 Service 层有没有事务注解再谈功能。5.3 扫码参数乱码中文和拼接符的编码十六戒现象学生扫二维码跳转到签到页taskId变成一团乱码后端接口报参数格式错误。原因二维码内容拼接了 URL比如https://xxx/?taskId1001name张三生成二维码时没做 URL 编码中文和符号在扫码解码后被截断或转成了非法字符。解决生成二维码时对整个链接做encodeURIComponent扫码后先decodeURIComponent再拆参数。如果二维码里只需要一个taskId就只放纯数字不要放大段字符串省去编码折腾// 解码取参数必须放在跳转前 const taskId decodeURIComponent(options.scene)scene参数是微信扫普通链接二维码跳小程序时自动带过来的习惯上把参数塞scene而不是 query因为scene本身做了编码转换能少踩一半坑。5.4 导出文件名乱码ResponseHeader 的 UTF-8 陷阱现象教师端导出 Excel下载下来文件名是_、%、Ã的混合体浏览器里完全没法认。原因后端设置Content-Disposition时直接写了中文文件名没有做 RFC 5987 编码。浏览器默认按 ASCII 解析响应头中文全乱。解决文件名参数用URLEncoder.encode编码再把替换成%20String fileName URLEncoder.encode(签到记录.xlsx, UTF-8).replace(, %20); response.setHeader(Content-Disposition, attachment; filename*UTF-8 fileName);注意是filename*带星号的写法这是 RFC 5987 的规范格式老写法filename只支持 ASCII。Excel 文件本身内容乱码是另一个问题那个要去检查 POI 写入时代码有没有指定UTF-8和响应头不是一回事。5.5 Web 管理端接口 401 连环炸OPTIONS 预检和 token 过期现象管理端页面能打开但所有请求都返回 401F12 里看到一堆红色的OPTIONS请求。原因Vue 管理端和后端接口跨域了浏览器会先发一个OPTIONS预检请求检查服务器允不允许跨域。你的拦截器把OPTIONS请求也拦了返回 401预检失败导致真实请求根本没发出去。token 过期也会 401但和这个现象混在一起很容易误判。解决拦截器放行OPTIONS请求跨域配置里允许来源、方法和头public boolean preHandle(...) { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } // 原有 token 校验 }判断方法是先看是不是 OPTIONS是就直接放行。另一步是给后端加跨域过滤器Access-Control-Allow-Origin别用**带不了Authorization头要写具体域名或者用allowedOriginPatterns(*)。6. 从能签到到好用签到率统计、导出与动态二维码改造6.1 用 SQL 把签到记录变成教学报表签到系统跑通之后真正被高频使用的是统计功能。教师想看的不是几十条流水而是一张表谁没来、谁迟到、哪门课签到率最低。MyBatis 的 XML 里写统计 SQL注意sign_record表只存成功记录所以算“应到人数”不能直接COUNT(*)得从任务关联的学生表去算。SELECT t.course_name, COUNT(DISTINCT r.student_id) AS 实际签到, sc.total AS 应到人数, ROUND(COUNT(DISTINCT r.student_id) / sc.total * 100, 2) AS 签到率 FROM sign_task t LEFT JOIN sign_record r ON r.task_id t.id AND r.status 1 LEFT JOIN ( SELECT task_id, COUNT(*) AS total FROM sign_student_map GROUP BY task_id ) sc ON sc.task_id t.id WHERE t.teacher_id #{teacherId} GROUP BY t.id, t.course_nameROUND(..., 2)保留两位小数GROUP BY t.id, t.course_name因为 task 主键唯一再加 course_name 不影响分组粒度。算迟到次数则比较签到时间和任务开始时间SELECT student_id, COUNT(*) AS late_count FROM sign_record r JOIN sign_task t ON r.task_id t.id WHERE r.sign_time DATE_ADD(t.start_time, INTERVAL 5 MINUTE) AND r.status 1 GROUP BY student_idDATE_ADD的 5 分钟是“宽限期”想严格点改成INTERVAL 0 MINUTE想宽松点改成 10。用参数传进去别写死。6.2 导出前把数据查出来再填 POI导出功能不需要在 Controller 里写大段业务逻辑更推荐把查询和导出拆开。Service 层返回ListSignRecordController 里只做响应头设置和 POI 填充ListSignRecord records signService.exportByTaskId(taskId); XSSFWorkbook workbook new XSSFWorkbook(); Sheet sheet workbook.createSheet(签到记录); Row header sheet.createRow(0); header.createCell(0).setCellValue(学号); header.createCell(1).setCellValue(姓名); header.createCell(2).setCellValue(签到时间); for (int i 0; i records.size(); i) { Row row sheet.createRow(i 1); SignRecord r records.get(i); row.createCell(0).setCellValue(r.getStudentNo()); row.createCell(1).setCellValue(r.getStudentName()); row.createCell(2).setCellValue( new SimpleDateFormat(yyyy-MM-dd HH:mm:ss).format(r.getSignTime())); }时间格式化放在 Java 层而不是 SQL 层数据库返回的Timestamp交给前端或 POI 时格式会走默认规则提前格式化成字符串最稳。大数据量导出用SXSSFWorkbook替代XSSFWorkbook可以避免几十万行数据时内存溢出。6.3 动态二维码防代签把固定内容改成有时效的令牌静态二维码的致命弱点是截图分享。学生把二维码截图发群里没到场的人也能扫码签到。一个低成本改造方案二维码内容不直接放taskId而是放一个短时有效的令牌。后端生成令牌String token UUID.randomUUID().toString().replace(-, ); redis.set(qr: token, taskId, 60); String qrContent https://yourdomain/sign?token token;二维码链接存活只有 60 秒过期后旧码作废。教师端每隔一分钟自动刷新二维码学生必须当场扫码才能签上。这个方案不需要改小程序端签到逻辑只需要在小程序里把参数从taskId换成token然后请求后端换取真实的taskIdconst token decodeURIComponent(options.token) request(/sign/resolveToken, POST, { token }).then(res { const taskId res.taskId // 继续走原签到流程 })resolveToken接口在 Redis 里查qr:前缀的 key查到就返回taskId并删掉 key保证一个码只能用一次。代码量不到二十行但代签这个最常见的漏洞就堵上了。这套资源把后端、小程序端、Vue 管理端和部署脚本都收在一个包里按第四步的操作顺序新手两个小时能跑通熟手可以接着改动态二维码和统计报表。从那以后我每次接手这类 SSM 毕业设计项目都会先盘一遍事务注解、时区参数和响应头编码这三件事再谈功能改造。这个习惯帮我省掉了很多“明明没问题”的深夜排查希望也能帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑