恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
SSM + 微信小程序学生签到系统实战:从数据库设计到定位签到与部署避坑
首页
资讯中心
/
SSM + 微信小程序学生签到系统实战:从数据库设计到定位签到与部署避坑
SSM + 微信小程序学生签到系统实战:从数据库设计到定位签到与部署避坑
发布时间:2026/10/8 6:21:22
简介这份资源是面向高校计算机相关专业学生与Java开发学习者的毕业设计/课程设计完整项目包主题为基于微信小程序的学生签到系统后端采用SSM框架实现。项目围绕签到管理场景覆盖学生微信授权登录、教师创建签到任务、一键与二维码签到、签到记录查询、消息提醒、师生权限区分以及签到率与迟到旷课统计等模块适合作为毕设选题参考或SSM与小程序联调的练手案例。压缩包共1087个文件约16.02MB包含120个Java后端源码、137个Vue前端页面、137个JS脚本以及44个wxss与43个wxml小程序页面文件另有sql建表脚本、properties配置、json数据与png、svg等界面素材前后端与数据库结构相对完整。目前已有132人学习下载。读者可据此梳理签到系统的业务分层与接口设计参考权限控制、统计分析与消息通知的实现思路并借助现成目录结构快速搭建运行环境、对照排查配置问题。1. 从一份 .rar 说起学生签到系统为什么还在用 SSM 微信小程序每年毕业季计算机相关专业的毕设题目里总有一批“学生签到系统”反复出现而技术栈几乎清一色是SSM 框架 微信小程序。很多人第一反应是“这玩意儿不是烂大街了吗”但真正动手做过的人会发现它恰好卡在一个很微妙的位置业务逻辑简单到能讲清楚工程链路又完整到能覆盖从数据库设计、后端接口、小程序端交互到部署联调的全流程。对于想快速跑通一个“能演示、能答辩、能二次开发”的完整项目的人来说这套组合的性价比其实很高。这份标题里的.rar压缩包通常包含后端 SSM 工程、小程序前端源码、SQL 建表脚本和一份说明文档。它解决的核心问题是教师发布签到任务学生在微信小程序端完成定位签到或数字签到后台实时统计出勤情况。适合的人群很明确——正在做毕设的学生、需要快速搭一套内部考勤原型的开发者以及想拿一个完整项目练手 SSM 与小程序联调的人。接下来我会按“先跑通、再改对、最后避坑”的顺序把这条链路拆开讲清楚。2. 把 SSM 后端跑起来从建表到第一个签到接口2.1 数据库设计三张核心表撑起整个签到逻辑拿到压缩包后不要急着打开 IDE 跑代码。先看 SQL 脚本因为签到系统的所有业务都围绕三张表展开用户表、签到任务表、签到记录表。很多同学跑不起来项目根因就是表结构没吃透字段对不上。常见做法是先用 Navicat 或命令行把库建好字符集统一用utf8mb4否则学生姓名里的生僻字会变成问号。下面是我一般会先执行的建表语句字段命名和索引都按实际查询场景做了调整-- 用户表同时存教师和学生用 role 区分 CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) DEFAULT NULL COMMENT 微信openid学生端登录用, username VARCHAR(32) NOT NULL COMMENT 学号或工号, password VARCHAR(64) NOT NULL COMMENT MD5加密存储, real_name VARCHAR(32) NOT NULL, role TINYINT NOT NULL DEFAULT 0 COMMENT 0学生 1教师, class_id INT DEFAULT NULL COMMENT 学生所属班级, UNIQUE KEY uk_username (username), KEY idx_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 签到任务表一次签到活动对应一条记录 CREATE TABLE sign_task ( id INT PRIMARY KEY AUTO_INCREMENT, teacher_id INT NOT NULL, title VARCHAR(128) NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, sign_type TINYINT NOT NULL DEFAULT 1 COMMENT 1普通签到 2定位签到 3数字签到, sign_code VARCHAR(8) DEFAULT NULL COMMENT 数字签到码, status TINYINT DEFAULT 1 COMMENT 1进行中 0已结束, KEY idx_teacher (teacher_id), KEY idx_time (start_time,end_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 签到记录表学生每签一次写一条用联合唯一索引防重复 CREATE TABLE sign_record ( id INT PRIMARY KEY AUTO_INCREMENT, task_id INT NOT NULL, student_id INT NOT NULL, sign_time DATETIME DEFAULT CURRENT_TIMESTAMP, latitude DECIMAL(10,7) DEFAULT NULL, longitude DECIMAL(10,7) DEFAULT NULL, status TINYINT DEFAULT 1 COMMENT 1正常 2迟到 3补签, UNIQUE KEY uk_task_student (task_id,student_id), KEY idx_student (student_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段 SQL 里有两个参数值得单独说。uk_task_student这个联合唯一索引是防重复签到的第一道防线比在 Java 层用if判断可靠得多因为并发场景下先查后插必然出问题。sign_type字段决定了后续接口的分支逻辑定位签到需要额外存经纬度数字签到需要校验sign_code普通签到则只记录时间。索引方面idx_time是为了教师端按时间段筛选任务时走索引不然数据量上来后列表查询会明显变慢。2.2 SSM 分层与签到接口实现SSM 的经典分层是 Controller → Service → Mapper配置文件包括applicationContext.xml、spring-mvc.xml、mybatis-config.xml和jdbc.properties。压缩包里的配置大概率用的是 MySQL 5.7 驱动如果你本地是 MySQL 8需要把驱动类改成com.mysql.cj.jdbc.Driver并在 JDBC URL 后面加上serverTimezoneAsia/Shanghai否则启动就报时区错误。签到接口是整个后端最核心的一段代码它要处理“任务是否在有效期内”“学生是否已签到”“签到类型对应的校验逻辑”三件事。我一般会把校验放在 Service 层Controller 只做参数接收和结果包装Service public class SignServiceImpl implements SignService { Autowired private SignTaskMapper signTaskMapper; Autowired private SignRecordMapper signRecordMapper; Override Transactional(rollbackFor Exception.class) public Result doSign(Integer taskId, Integer studentId, String signCode, BigDecimal lat, BigDecimal lng) { // 1. 查任务判断是否存在且在有效期内 SignTask task signTaskMapper.selectById(taskId); if (task null) { return Result.fail(签到任务不存在); } Date now new Date(); if (now.before(task.getStartTime()) || now.after(task.getEndTime())) { return Result.fail(不在签到时间范围内); } // 2. 防重复先查一次数据库唯一索引兜底 SignRecord exist signRecordMapper.selectByTaskAndStudent(taskId, studentId); if (exist ! null) { return Result.fail(请勿重复签到); } // 3. 按签到类型分支校验 if (task.getSignType() 3) { if (!task.getSignCode().equals(signCode)) { return Result.fail(签到码错误); } } // 4. 写入记录定位签到才存经纬度 SignRecord record new SignRecord(); record.setTaskId(taskId); record.setStudentId(studentId); record.setSignTime(now); if (task.getSignType() 2) { record.setLatitude(lat); record.setLongitude(lng); } signRecordMapper.insert(record); return Result.ok(签到成功); } }Transactional注解在这里是必须的因为“查重 插入”是一个原子操作虽然唯一索引能兜底但事务能保证异常时回滚干净。sign_type的判断顺序也有讲究先判数字签到再判定位签到最后走普通签到这样扩展新类型时只需要加分支不用动主流程。参数lat和lng用BigDecimal而不是double是因为经纬度精度要求高double在序列化时容易出现精度丢失导致定位签到算距离时偏差几十米。2.3 用 Postman 验证接口再动小程序后端写完不要直接开小程序模拟器先用 Postman 把接口跑通。启动 Tomcat 后请求http://localhost:8080/sign/dosignBody 用 form-data 传taskId、studentId、signCode。如果返回{code:200,msg:签到成功}说明后端链路没问题。这一步能帮你把“后端问题”和“前端问题”隔离开不然联调时两边报错排查起来就是黑匣子。常见的一个翻车点是中文乱码。如果返回的msg是问号检查spring-mvc.xml里有没有配StringHttpMessageConverter的supportedMediaTypes为text/plain;charsetUTF-8以及 Tomcat 的server.xml里Connector有没有加URIEncodingUTF-8。这两个地方漏一个中文就废了。3. 微信小程序端登录、签到页与请求封装3.1 微信小程序登录获取手机号与 openid 绑定小程序端的登录流程和普通网页完全不同它依赖wx.login拿到的临时code再传给后端换openid。很多压缩包里的代码只做了wx.login就完事没有把openid和sys_user表关联起来导致学生换设备后签到记录对不上。正确的做法是后端提供一个/wx/login接口接收code调用微信的jscode2session接口换回openid然后查sys_user表如果openid不存在就绑定当前登录的学号。// 小程序端登录逻辑 wx.login({ success(res) { if (res.code) { wx.request({ url: http://localhost:8080/wx/login, method: POST, data: { code: res.code, username: 2021001 }, success(r) { // 后端返回 openid 和用户信息存到全局 getApp().globalData.userInfo r.data.data; wx.setStorageSync(token, r.data.data.token); } }); } } });这里有个参数要注意jscode2session需要appid和secret这两个值放在后端配置文件里不要写在小程序端否则泄露后别人能冒充你的小程序。另外wx.login的code只能用一次五分钟内有效所以后端换openid失败时要提示用户重新登录而不是反复用同一个code重试。3.2 签到页的交互与定位授权签到页通常有一个大按钮点击后根据任务类型弹出不同交互普通签到直接提交数字签到弹输入框定位签到先调wx.getLocation。定位授权是小程序里最容易踩坑的地方因为用户一旦拒绝后续再调wx.getLocation会直接走fail回调不会再次弹窗。// 定位签到先检查授权再获取位置 wx.getSetting({ success(res) { if (!res.authSetting[scope.userLocation]) { wx.authorize({ scope: scope.userLocation, success() { getLocationAndSign(); }, fail() { wx.showModal({ title: 需要定位权限, content: 请在设置中开启定位后重新签到, success(m) { if (m.confirm) wx.openSetting(); } }); } }); } else { getLocationAndSign(); } } }); function getLocationAndSign() { wx.getLocation({ type: gcj02, success(pos) { wx.request({ url: http://localhost:8080/sign/dosign, method: POST, data: { taskId: 1, studentId: 1001, latitude: pos.latitude, longitude: pos.longitude }, success(r) { wx.showToast({ title: r.data.msg }); } }); } }); }type: gcj02这个参数必须写因为微信默认返回的是wgs84坐标系而国内地图和距离计算通常用gcj02不统一坐标系会导致签到距离算出来偏差几百米。wx.openSetting只能在用户主动点击后调用所以要用showModal引导不能直接跳设置页。3.3 请求封装与顶部导航栏高度适配小程序里如果每个页面都写一遍wx.request后期改域名或加 token 会改到崩溃。我一般会封装一个request.js统一处理 baseURL、token 注入和错误提示。另外微信小程序顶部导航栏高度在不同机型上不一样自定义导航栏时需要用wx.getSystemInfoSync拿状态栏高度再算上胶囊按钮的位置否则标题会被刘海挡住。// utils/request.js const BASE_URL http://localhost:8080; function request(options) { const token wx.getStorageSync(token); return new Promise((resolve, reject) { wx.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Authorization: token || }, success(res) { if (res.data.code 200) resolve(res.data); else { wx.showToast({ title: res.data.msg, icon: none }); reject(res.data); } }, fail(err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); } module.exports { request };封装后页面里只需要request({ url: /sign/dosign, method: POST, data: {...} })token 自动带上错误统一提示。导航栏高度适配的常见做法是statusBarHeight从系统信息里取导航栏内容高度固定 44px总高度就是两者之和然后把这个值设给自定义导航栏的padding-top。4. 联调与部署让签到系统真正跑在手机上4.1 本地联调关闭域名校验与真机调试小程序开发工具默认校验合法域名本地localhost不在白名单里所以要在“详情 → 本地设置”里勾选“不校验合法域名”。真机调试时手机和电脑必须在同一个局域网把BASE_URL改成电脑的局域网 IP比如http://192.168.1.100:8080同时确保防火墙放行 8080 端口。如果真机上请求超时先检查电脑防火墙再检查 Tomcat 是否绑定了0.0.0.0而不是127.0.0.1。Tomcat 默认监听所有网卡但有些同学改过server.xml里的address属性就会导致只有本机能访问。4.2 部署到服务器打包与数据库迁移正式部署时后端打成war包丢进 Tomcat 的webapps目录数据库用mysqldump导出再导入服务器。小程序端需要把BASE_URL改成正式域名并在微信公众平台配置request合法域名。注意域名必须备案且支持 HTTPS否则小程序正式版无法请求。# 导出本地数据库 mysqldump -u root -p sign_db sign_db.sql # 服务器上导入 mysql -u root -p sign_db sign_db.sql # 打包后端Maven 项目 mvn clean package -DskipTests # 把 war 包复制到 Tomcat cp target/sign.war /usr/local/tomcat/webapps/-DskipTests在部署时常用因为单元测试可能依赖本地环境服务器上跑会失败。但第一次部署建议先不加这个参数确认测试能过再跳过不然可能把编译错误带到线上。4.3 签到距离校验的服务端实现定位签到如果只存经纬度不校验距离学生可以在宿舍签教室的到。服务端需要根据教师设置的签到点和允许半径用 Haversine 公式算球面距离。这个逻辑放在 Service 层和签到主流程分开方便单独测试。// 计算两点间距离单位米 private static final double EARTH_RADIUS 6371000; public static double getDistance(double lat1, double lng1, double lat2, double lng2) { double radLat1 Math.toRadians(lat1); double radLat2 Math.toRadians(lat2); double deltaLat radLat1 - radLat2; double deltaLng Math.toRadians(lng1) - Math.toRadians(lng2); double a Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) Math.cos(radLat1) * Math.cos(radLat2) * Math.sin(deltaLng / 2) * Math.sin(deltaLng / 2); double c 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return EARTH_RADIUS * c; }EARTH_RADIUS用 6371000 米是通用近似值误差在可接受范围内。Math.toRadians把角度转弧度因为三角函数入参是弧度。算出来的距离和教师设置的allow_radius比较超过就拒绝签到。这个校验必须放服务端小程序端的定位可以被模拟不能信任客户端传来的坐标。5. 避坑与排查签到系统上线前必须过的五道坎5.1 现象学生点击签到没反应控制台报 400原因通常是请求参数类型不匹配。小程序端wx.request默认content-type是application/json而后端 Controller 用RequestParam接收Spring 解析不了 JSON body 里的字段。解决方式有两种要么后端改用RequestBody接收一个 DTO要么小程序端把header改成application/x-www-form-urlencoded。我一般选后者改动最小。5.2 现象签到记录里出现同一学生两条数据原因是并发请求下“先查后插”失效两个请求同时查到“未签到”然后都执行插入。虽然uk_task_student唯一索引能拦住第二条但会抛DuplicateKeyException如果没捕获前端看到的是 500 错误而不是“请勿重复签到”。解决方式是在 Service 里try-catch这个异常返回友好提示同时前端按钮点击后立即置灰防止用户连点。5.3 现象定位签到距离算出来总是差几百米坐标系不统一导致的。微信wx.getLocation默认返回wgs84而教师设置的签到点可能是从地图上取的gcj02坐标。两者直接算距离必然偏。解决方式是统一用gcj02小程序端type参数写死gcj02教师端设置签到点时也用同一坐标系。5.4 现象小程序真机上请求后端超时开发工具正常开发工具走的是电脑网络真机走的是手机网络。常见原因是电脑防火墙拦了 8080 端口或者手机和电脑不在同一网段。排查顺序先用手机浏览器访问http://电脑IP:8080如果能打开说明网络通问题在小程序配置如果打不开先关防火墙再试。另外有些公司 WiFi 开启了 AP 隔离设备之间不能互访这种情况只能换热点。5.5 现象部署到服务器后中文全部乱码数据库字符集、JDBC URL、Tomcat 编码三处只要有一处不是utf8mb4就会乱。排查时先看数据库SHOW VARIABLES LIKE character%再看 JDBC URL 有没有characterEncodingutf8最后看 Tomcat 的URIEncoding。三处都对齐后重启 Tomcat乱码基本消失。6. 进阶技巧把签到系统做成能复用的考勤底座跑通基础签到后如果想让这个项目在答辩或实际使用中更有说服力可以往两个方向加东西。第一个是签到统计导出教师端按班级导出 Excel用 Apache POI 或者 EasyExcel 都行EasyExcel 内存占用更低适合数据量大的场景。第二个是签到提醒用微信订阅消息在签到开始前推一条通知给学生这个需要在小程序后台申请模板 ID后端调subscribeMessage.send接口。// 用 EasyExcel 导出签到记录 public void exportSignRecords(HttpServletResponse response, Integer taskId) throws IOException { ListSignRecordVO list signRecordMapper.selectByTask(taskId); response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setCharacterEncoding(utf-8); String fileName URLEncoder.encode(签到记录, UTF-8); response.setHeader(Content-Disposition, attachment;filename fileName .xlsx); EasyExcel.write(response.getOutputStream(), SignRecordVO.class) .sheet(签到明细) .doWrite(list); }Content-Disposition里的文件名必须URLEncoder.encode否则中文文件名在部分浏览器上会变成乱码。EasyExcel.write的第二个参数是映射的 VO 类字段上加ExcelProperty(学号)注解就能控制表头。还有一个容易被忽略的点签到系统的时区。服务器如果是 UTC 时间而sign_task的start_time存的是北京时间比较时就会差 8 小时。我一般会在 JDBC URL 里加serverTimezoneAsia/Shanghai并且 JVM 启动参数加-Duser.timezoneAsia/Shanghai双保险。最后说一个我自己的习惯每次改完签到逻辑先用 Postman 跑一遍正常签到、重复签到、超时签到、错误签到码四个用例再开小程序点一遍。这四个用例覆盖了 90% 的线上问题比漫无目的地翻日志快得多。希望帮到你。本文还有配套的精品资源点击获取