☰
课堂签到小程序源码实战:四种签到模式与SSM后台工程取舍
2026/10/22 4:40:40 网站建设 项目流程

简介:这份资源是课堂签到微信小程序完整源码包,后台采用SSM框架与MySQL数据库开发,面向计算机、软件工程等专业学生及需要课程设计、期末大作业或毕业设计参考的开发者。项目实现了限时签到、密码签到、手势签到与位置签到四种模式,覆盖签到场景中的常见业务需求,适合作为Java Web与微信小程序全栈学习的实战案例。压缩包共304个文件,约13.56MB,包含40个java源文件、28个xml配置、33个jar依赖、27个js与25个json脚本,以及22个wxml、23个wxss页面样式文件,另有sql建表脚本、properties配置与jsp页面,前后端结构完整。目前已有179人学习下载。读者可从中获取小程序端页面与交互逻辑、SSM分层架构与MyBatis映射配置、签到业务数据表设计及接口调用方式,便于快速理解项目整体流程并进行二次开发与调试。

1. 课堂签到小程序:四种签到模式背后的工程取舍

课堂签到这件事,看起来简单,真做起来全是细节。老师想要的是三秒发起、学生无法代签、后台能导出考勤表;学生想要的是打开就能签、不卡顿、不泄露位置。一个「课堂签到微信小程序源码+项目说明」的标题背后,其实是一套完整的教务场景闭环:小程序端负责交互与定位,SSM 后台负责签到规则、记录落库和统计导出,MySQL 负责把每一次签到行为变成可追溯的数据。限时签到、密码签到、手势签到、位置签到这四种模式,分别对应不同的课堂纪律强度和技术复杂度。适合谁看?正在做课程设计的学生、需要快速交付教务工具的开发者、以及想理解小程序+Java 后台完整链路的工程师。下面按「先跑通再优化」的顺序,把选型、建表、接口、定位和踩坑一次讲透。

2. 四种签到模式怎么选:从限时到位置的适用边界

2.1 限时签到与密码签到:最低成本的防代签

限时签到的核心逻辑是「在指定时间窗口内允许提交」,通常由教师端设置一个倒计时,比如 60 秒或 120 秒。它的技术实现最简单:后端记录签到开始时间,学生提交时校验当前时间是否在窗口内。但它的防代签能力很弱——学生完全可以把签到码截图发给室友。所以限时签到一般配合动态口令使用,口令每 10 秒刷新一次,截图就失效了。

密码签到则是教师口播一个 4 到 6 位数字,学生在小程序里输入。它的优点是零硬件依赖,缺点是密码一旦被转发到班级群,签到就形同虚设。实际项目中,密码签到适合大班额、纪律要求不高的选修课,或者作为限时签到的降级方案——当定位服务不可用时,教师可以临时切换到密码签到。

从数据库设计看,这两种模式可以共用一张签到记录表,用sign_type字段区分。教师端发起签到时,写入一条sign_task记录,包含type、start_time、end_time、password(加密存储)、duration等字段。学生提交时,后端先查任务是否有效,再校验密码或时间窗口。

CREATE TABLE sign_task ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_id BIGINT NOT NULL, teacher_id BIGINT NOT NULL, sign_type TINYINT NOT NULL COMMENT '1限时 2密码 3手势 4位置', password VARCHAR(64) DEFAULT NULL COMMENT '加密后的密码', start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, duration INT DEFAULT 60 COMMENT '限时秒数', status TINYINT DEFAULT 1 COMMENT '1进行中 0已结束', create_time DATETIME DEFAULT CURRENT_TIMESTAMP );

参数说明:sign_type用 tinyint 而不是枚举字符串,是为了索引效率;password存 BCrypt 哈希而不是明文,防止数据库泄露后被直接利用;end_time在限时签到中由start_time + duration计算得出,避免客户端篡改。

2.2 手势签到与位置签到:防代签的进阶方案

手势签到借鉴了 Android 解锁图案的思路:教师端设置一个 3x3 或 4x4 的点阵图案,学生需要按顺序连接正确的点。它的防代签能力比密码强,因为手势序列不容易用文字描述转发。但实现上要注意:手势坐标需要归一化处理,否则不同屏幕尺寸的小程序端会解析出不同结果。

位置签到是四种模式里技术复杂度最高的。它依赖微信小程序的wx.getLocation接口获取经纬度,后端计算学生位置与教师位置的距离,超过阈值则拒绝签到。常见做法是教师端发起签到时记录当前坐标,学生端提交时带上自己的坐标,后端用 Haversine 公式计算球面距离。

// 小程序端获取位置并提交 wx.getLocation({ type: 'gcj02', success: (res) => { const { latitude, longitude } = res; wx.request({ url: 'https://your-domain.com/api/sign/location', method: 'POST', data: { taskId: this.data.taskId, latitude, longitude, studentId: this.data.studentId }, success: (resp) => { if (resp.data.code === 200) { wx.showToast({ title: '签到成功' }); } else { wx.showToast({ title: resp.data.msg, icon: 'none' }); } } }); }, fail: () => { wx.showToast({ title: '定位失败,请检查权限', icon: 'none' }); } });

逻辑说明:type: 'gcj02'返回的是国测局坐标系,与微信内置地图一致,不要用wgs84,否则会有几百米偏移。提交时把taskId和studentId一起传给后端,后端先校验任务状态,再计算距离。参数说明:距离阈值一般设为 100 到 300 米,太小会因 GPS 漂移误判,太大则失去约束意义。教师端坐标建议取多次定位的平均值,减少单次误差。

2.3 SSM 后台的接口分层与签到任务状态机

SSM 框架下,签到模块的接口通常分三层:Controller 接收小程序请求,Service 处理业务逻辑,DAO 操作数据库。签到任务的状态流转是核心:教师发起签到后,任务状态为「进行中」;到达end_time后,定时任务或惰性判断将其置为「已结束」;学生提交时,Service 层先查任务状态,再根据sign_type走不同的校验分支。

@Service public class SignServiceImpl implements SignService { @Autowired private SignTaskMapper signTaskMapper; @Autowired private SignRecordMapper signRecordMapper; @Override @Transactional public Result sign(Long taskId, Long studentId, String password, Double lat, Double lng) { SignTask task = signTaskMapper.selectById(taskId); if (task == null || task.getStatus() == 0) { return Result.fail("签到已结束"); } if (System.currentTimeMillis() > task.getEndTime().getTime()) { return Result.fail("超出签到时间"); } // 防重复签到 SignRecord exist = signRecordMapper.selectByTaskAndStudent(taskId, studentId); if (exist != null) { return Result.fail("请勿重复签到"); } // 按类型校验 if (task.getSignType() == 2) { if (!BCrypt.checkpw(password, task.getPassword())) { return Result.fail("密码错误"); } } else if (task.getSignType() == 4) { double distance = GeoUtil.haversine(lat, lng, task.getLat(), task.getLng()); if (distance > task.getRadius()) { return Result.fail("不在签到范围内"); } } SignRecord record = new SignRecord(); record.setTaskId(taskId); record.setStudentId(studentId); record.setSignTime(new Date()); signRecordMapper.insert(record); return Result.success("签到成功"); } }

逻辑说明:@Transactional保证签到记录插入与任务状态更新的一致性;防重复签到用数据库唯一索引兜底,而不是只靠查询,避免并发下两个请求同时通过检查。参数说明:radius字段存在sign_task表里,默认 200 米,教师端可调;BCrypt.checkpw的第二个参数是数据库里存的哈希值,不要自己写 MD5 比较。

3. 从零跑通:数据库建表、后台接口与小程序联调

3.1 建表脚本与索引设计

除了sign_task,还需要sign_record记录每次签到,course和student表管理基础信息。sign_record的关键索引是(task_id, student_id)唯一索引,防止同一学生重复签到;以及(student_id, sign_time)普通索引,用于查询某学生的考勤历史。

CREATE TABLE sign_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, task_id BIGINT NOT NULL, student_id BIGINT NOT NULL, sign_time DATETIME DEFAULT CURRENT_TIMESTAMP, latitude DOUBLE DEFAULT NULL, longitude DOUBLE DEFAULT NULL, device_info VARCHAR(255) DEFAULT NULL, UNIQUE KEY uk_task_student (task_id, student_id), KEY idx_student_time (student_id, sign_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

参数说明:device_info存小程序端传来的设备型号和系统版本,用于事后排查异常签到;latitude和longitude只在位置签到时有值,其他模式为 NULL。字符集用utf8mb4而不是utf8,因为学生姓名可能包含生僻字。

3.2 教师端发起签到的接口与参数校验

教师端发起签到是一个 POST 接口,接收courseId、signType、duration、password、radius等参数。Controller 层用@Valid做基础校验,Service 层做业务校验:比如限时签到的duration不能超过 300 秒,位置签到的radius不能小于 50 米。

@RestController @RequestMapping("/api/teacher/sign") public class TeacherSignController { @Autowired private SignService signService; @PostMapping("/create") public Result create(@RequestBody @Valid SignTaskDTO dto) { if (dto.getSignType() == 1 && dto.getDuration() > 300) { return Result.fail("限时签到最长300秒"); } if (dto.getSignType() == 4 && dto.getRadius() < 50) { return Result.fail("位置签到半径不得小于50米"); } return signService.createTask(dto); } }

逻辑说明:参数校验放在 Controller 和 Service 两层,Controller 做格式校验,Service 做业务规则校验。参数说明:SignTaskDTO里的password字段在 Service 层用 BCrypt 加密后再入库,不要直接存明文。

3.3 小程序端签到页面的状态管理

小程序端需要处理四种签到模式的 UI 切换。建议用一个signType变量控制页面渲染:限时签到显示倒计时,密码签到显示输入框,手势签到显示 canvas 点阵,位置签到显示地图和距离提示。状态管理用Page的data对象即可,不需要引入额外状态库。

Page({ data: { signType: 0, countdown: 0, password: '', gesturePath: [], distance: null }, onLoad(options) { this.setData({ signType: Number(options.type) }); if (this.data.signType === 1) { this.startCountdown(options.endTime); } }, startCountdown(endTime) { const timer = setInterval(() => { const left = Math.max(0, Math.floor((endTime - Date.now()) / 1000)); this.setData({ countdown: left }); if (left <= 0) { clearInterval(timer); wx.showToast({ title: '签到已结束', icon: 'none' }); } }, 1000); } });

逻辑说明:onLoad从页面参数里取type和endTime,限时签到启动倒计时。参数说明:endTime是后端返回的时间戳,不要用客户端本地时间计算,否则学生改系统时间就能绕过。

4. 避坑与排查:定位漂移、重复签到和并发问题

4.1 定位漂移导致签到失败

现象:学生在教室门口签到,提示「不在签到范围内」,但明明就在教师旁边。原因:微信小程序wx.getLocation返回的坐标受基站和 Wi-Fi 影响,室内误差可能达到 100 到 500 米。解决:把radius默认值设为 300 米,教师端发起签到时取 5 次定位的平均值,学生端提交前先判断accuracy字段,如果大于 100 米则提示「定位精度不足,请到窗边重试」。

4.2 重复签到与并发插入

现象:学生快速点击签到按钮,数据库里出现两条记录。原因:前端没有防抖,后端查询和插入之间有并发窗口。解决:前端按钮点击后立即置灰,后端用INSERT ... ON DUPLICATE KEY UPDATE或捕获唯一索引冲突异常,返回「请勿重复签到」。

4.3 手势签到坐标归一化错误

现象:教师端设置的手势在部分手机上无法识别。原因:canvas 的坐标没有按屏幕宽度归一化,不同分辨率下解析出的点阵序列不同。解决:存储手势时只存点的索引(0 到 8),不存像素坐标;学生端解析时按索引匹配,与屏幕尺寸无关。

4.4 限时签到倒计时与服务器时间不同步

现象:学生手机时间比服务器快 30 秒,倒计时结束后提交仍然成功。原因:后端只校验了end_time,没有校验start_time。解决:后端同时校验start_time <= now <= end_time,并且end_time由服务器生成,不信任客户端传入的时间。

4.5 密码签到的密码泄露

现象:教师口播密码后,班级群里立刻出现密码截图。原因:密码是静态的,且没有绑定学生身份。解决:密码签到改为「动态口令」模式,后端每 10 秒生成一个新口令,教师端显示当前口令,学生端提交时校验口令和时间窗口。这样截图转发后,口令已经过期。

5. 进阶技巧:用 Redis 缓存签到任务与考勤导出优化

签到任务的读多写少特性非常适合用 Redis 缓存。教师发起签到后,把sign_task的完整信息写入 Redis,设置 TTL 为duration + 60秒。学生提交时,Service 层先查 Redis,命中则直接校验,未命中再查 MySQL 并回写。这样在高并发签到场景下,数据库压力能降低一个数量级。

public SignTask getTaskFromCache(Long taskId) { String key = "sign:task:" + taskId; String json = redisTemplate.opsForValue().get(key); if (json != null) { return JSON.parseObject(json, SignTask.class); } SignTask task = signTaskMapper.selectById(taskId); if (task != null) { redisTemplate.opsForValue().set(key, JSON.toJSONString(task), task.getDuration() + 60, TimeUnit.SECONDS); } return task; }

参数说明:TTL 设为duration + 60是为了覆盖签到窗口结束后的一分钟缓冲,防止边界时间提交失败。注意 Redis 里的password字段仍然是 BCrypt 哈希,不要因为缓存就存明文。

考勤导出是教师端的高频需求。常见做法是用 POI 或 EasyExcel 生成 Excel,字段包括学号、姓名、签到时间、签到状态。优化点在于:不要每次导出都查全量记录,而是按task_id分页查询,每页 500 条,边查边写。另外,缺勤学生的判定逻辑是「在sign_record里没有对应记录」,可以用LEFT JOIN一次查出。

SELECT s.student_no, s.name, r.sign_time, CASE WHEN r.id IS NULL THEN '缺勤' ELSE '已签到' END AS status FROM student s LEFT JOIN sign_record r ON s.id = r.student_id AND r.task_id = ? WHERE s.course_id = ? ORDER BY s.student_no;

逻辑说明:LEFT JOIN保证没有签到记录的学生也会出现在结果里,CASE WHEN生成状态列。参数说明:?分别是task_id和course_id,导出时按课程和任务双重过滤,避免跨课程数据混淆。

最后说一个我踩过的坑:早期版本把签到任务的end_time交给客户端计算,结果有学生把手机时间调慢,倒计时结束后还能签到。后来改成后端返回服务器时间戳,客户端只做展示,所有校验都在 Service 层用System.currentTimeMillis()完成。这个习惯一直保留到现在——凡是涉及规则判断的逻辑,永远不要信任客户端传来的时间、坐标和状态。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询