简介:这份资源是课堂签到微信小程序完整源码包,后台采用SSM框架与MySQL数据库开发,面向计算机、软件工程等专业学生及需要课程设计、期末大作业或毕业设计参考的开发者。项目实现了限时签到、密码签到、手势签到与位置签到四种模式,覆盖签到场景中的常见业务需求,适合作为Java Web与微信小程序联合开发的学习范例。压缩包共304个文件,约13.56MB,包含40个java源文件与74个class编译文件构成后台核心逻辑,28个xml与6个properties负责框架配置,前端由22个wxml、23个wxss与27个js组成小程序页面与交互,另有33个jar依赖、1个sql建表脚本及图片资源,结构完整可直接导入运行。目前已有179人学习关注。通过阅读源码,读者可掌握SSM分层架构、微信小程序接口调用、签到状态判定与位置校验等实现思路,并在此基础上自行扩展考勤统计、消息提醒等功能,是兼顾学习与二次开发的实用参考资料。
1. 课堂签到小程序源码拆包:四种签到模式到底怎么落地
期末周前两周,某高校一位带课的老师找我,说他那门 120 人的选修课点名要花十分钟,想搞个扫码签到又怕学生代签。我翻出这份课堂签到微信小程序源码,后台是 SSM 框架加 MySQL,前端小程序端,签到方式给了四种:限时签到、密码签到、手势签到、位置签到。这不是玩具 Demo,四种模式对应四种防代签强度,从「拼手速」到「拼地理位置」层层加码。适合谁?做课程设计、毕业设计的学生,或者想给自己小项目加一套签到模块的开发者。源码包结构完整,APIController.class、APIService.class以及一堆Example类都在,说明 MyBatis 逆向工程生成的实体和条件查询都齐了。下面我按「先跑起来、再拆逻辑、最后避坑」的顺序,把这份资源里里外外讲透,你照着复现不会卡在环境上。
2. 环境搭建与数据库初始化:从零把 SSM 后台跑起来
2.1 技术栈确认与依赖版本选择
拿到源码先别急着导入 IDE,看一眼目录结构。后台是标准 SSM:Spring + SpringMVC + MyBatis,数据库 MySQL。APIController.class说明接口层用注解方式暴露 HTTP 接口,APIService.class是业务层,LsignExample、PsignExample、GsignExample、CreatelsignExample这些类名对应四种签到模式的数据表操作。Example后缀是 MyBatis Generator 生成的,用来拼where条件,省得手写 SQL。
我一般会先确认三样东西:JDK 版本、Tomcat 版本、MySQL 版本。这份源码没有附带pom.xml的明确版本号,但 SSM 项目常见搭配是 JDK 8、Tomcat 8.5、MySQL 5.7。如果你用 JDK 11 以上,Spring 4.x 的某些反射会报IllegalAccessError,这是血泪经验,别硬扛,直接换 JDK 8。
数据库连接配置在jdbc.properties或applicationContext.xml里,找jdbc.url、jdbc.username、jdbc.password三个键。常见做法是把jdbc.url改成jdbc:mysql://localhost:3306/checkin?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai,库名checkin可以自己定,但要和建库语句一致。
2.2 建库建表与初始数据导入
源码包里一般带一个.sql文件,如果没有,就根据Example类反推表结构。四种签到模式至少对应四张业务表:限时签到表、密码签到表、手势签到表、位置签到表,外加用户表users和班级表classes。UsersExample和ClassesExample的存在证实了这两张基础表。
建库语句我习惯这样写:
CREATE DATABASE checkin DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE checkin; -- 用户表:存学生和教师的基本信息 CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) UNIQUE COMMENT '微信openid,小程序端唯一标识', name VARCHAR(32) NOT NULL, role TINYINT DEFAULT 0 COMMENT '0学生 1教师', class_id INT ); -- 班级表 CREATE TABLE classes ( id INT PRIMARY KEY AUTO_INCREMENT, class_name VARCHAR(64) NOT NULL, teacher_id INT ); -- 限时签到表:记录每次发起的签到任务 CREATE TABLE lsign ( id INT PRIMARY KEY AUTO_INCREMENT, class_id INT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, duration INT COMMENT '有效时长(秒)' );参数说明:openid是小程序端wx.login换取的唯一标识,用来区分谁签到了;duration控制限时签到的窗口期,比如设 120 秒,学生必须在两分钟内完成。start_time和end_time是冗余字段,方便直接查「当前是否有进行中的签到」,不用每次算start_time + duration。
导入初始数据时,至少插一条教师账号和一条学生账号,openid可以先填假值,等小程序端联调时再替换。注意utf8mb4而不是utf8,因为学生姓名里可能有生僻字。
2.3 后台启动与接口连通性验证
把项目导入 Eclipse 或 IDEA,配置 Tomcat,部署后访问http://localhost:8080/项目名/。如果看到 404 但控制台没报错,检查web.xml里DispatcherServlet的url-pattern是不是/。SSM 项目常见坑是spring-mvc.xml里没开注解驱动,导致APIController里的@RequestMapping不生效。
验证接口连通性,我一般用 Postman 或 curl 打一个最简单的 GET:
curl -X POST http://localhost:8080/checkin/api/login \ -H "Content-Type: application/json" \ -d '{"openid":"test_openid_001","name":"测试学生"}'如果返回 JSON 且code为 200,说明 SpringMVC 和 MyBatis 链路通了。如果报NoSuchBeanDefinitionException,检查APIService有没有加@Service注解,或者applicationContext.xml的组件扫描包路径对不对。这一步过了,再往下拆四种签到模式才有意义。
3. 四种签到模式的后端逻辑拆解:从 Controller 到 Example 查询
3.1 限时签到与密码签到的接口设计差异
限时签到的核心是「时间窗口」。APIController里应该有一个createLsign接口,教师端调用,传入class_id和duration,后端算出start_time和end_time落库。学生端调doLsign,后端先查当前时间是否在start_time和end_time之间,再查该学生是否已签过。LsignExample就是用来拼这个查询条件的:
// 伪代码示意,实际类名以源码为准 LsignExample example = new LsignExample(); example.createCriteria() .andClassIdEqualTo(classId) .andStartTimeLessThanOrEqualTo(new Date()) .andEndTimeGreaterThanOrEqualTo(new Date()); List<Lsign> activeList = lsignMapper.selectByExample(example);参数说明:andStartTimeLessThanOrEqualTo和andEndTimeGreaterThanOrEqualTo组合起来就是「当前时间落在窗口内」。如果activeList为空,说明没有进行中的签到,直接返回错误码。
密码签到多了一个password字段。教师发起时生成一个随机 6 位数字,存到psign表。学生签到时,后端先校验密码是否匹配,再校验时间窗口。PsignExample的查询条件里会多一个andPasswordEqualTo。这里有个设计选择:密码是明文存还是哈希存?课程设计级别通常明文,但我会建议至少加个 MD5,防止数据库被翻出来直接看到密码。不过源码里大概率是明文,你如果要改,记得前后端一起改。
3.2 手势签到与位置签到的校验链路
手势签到稍微复杂一点。教师端设置一个手势路径,比如「1-2-3-5-7-8-9」这种九宫格连线,存成字符串。学生端在小程序里画出同样的手势,前端把路径字符串传给后端,后端和数据库里的比对。GsignExample对应的表里应该有一个gesture_path字段。
位置签到的校验链路最长:教师端发起签到时,获取当前经纬度,存到lsign或单独的位置签到表。学生端签到时,小程序调wx.getLocation拿到自己的经纬度,传给后端。后端计算两点距离,如果超过阈值(比如 100 米),拒绝签到。距离计算我一般用 Haversine 公式:
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); // Haversine 核心公式,算球面两点最短距离 double a = Math.pow(Math.sin(deltaLat / 2), 2) + Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(deltaLng / 2), 2); return 2 * EARTH_RADIUS * Math.asin(Math.sqrt(a)); }参数说明:lat1、lng1是教师发起签到时的坐标,lat2、lng2是学生签到时的坐标。EARTH_RADIUS取 6371000 米是常用值,精度足够课堂场景。阈值建议设 100 到 200 米,太小了 GPS 漂移会导致签不上,太大了隔壁教室也能签。这是玄学,得实测调。
3.3 Example 条件查询的复用与扩展
CreatelsignExample、CreatepsignExample、CreategsignExample这三个类名看起来是「创建签到」时的条件构造器,和LsignExample、PsignExample、GsignExample成对出现。MyBatis Generator 会为每张表生成一个XxxExample和一个Xxx实体类,Criteria是内部类,用来拼and条件。
复用技巧:如果你要加一种新签到模式,比如「扫码签到」,照着现有模式复制一套Example、Mapper、Service、Controller就行。关键是Mapper.xml里的resultMap和Base_Column_List要改对,否则查出来的字段全是 null。我见过有人只改了 Java 类没改 XML,调了一下午以为数据库没数据,其实是映射没对上。
扩展时注意Example的orderByClause,比如查最近一次签到,可以设example.setOrderByClause("start_time desc"),再配合limit 1。不过 MyBatis 的Example不直接支持limit,得在Mapper接口里手写一个selectLatest方法。这是常见做法,别在Example上死磕。
4. 小程序端联调与接口对接:请求封装与状态管理
4.1 请求封装与全局配置
小程序端一般有一个utils/request.js或类似文件,封装wx.request。我习惯把后台地址抽成常量,方便切换开发和生产环境:
// config.js const BASE_URL = 'http://localhost:8080/checkin/api'; // request.js const request = (url, method, data) => { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method: method, data: data, header: { 'Content-Type': 'application/json' }, success: res => { if (res.data.code === 200) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' }); reject(res.data); } }, fail: err => reject(err) }); }); };参数说明:BASE_URL里的localhost在真机调试时要换成电脑的局域网 IP,比如192.168.1.100,否则手机访问不到。header里Content-Type必须是application/json,和后端@RequestBody对应。code === 200是约定好的成功码,后端APIController里统一返回这个结构。
4.2 四种签到模式的页面跳转与参数传递
小程序端通常有一个签到首页,列出当前进行中的签到任务,点进去根据type字段跳不同页面。type可以是lsign、psign、gsign、location。跳转时把signId传过去:
// 首页点击签到任务 wx.navigateTo({ url: `/pages/sign/detail?type=${item.type}&signId=${item.id}` });在detail页面onLoad里拿参数:
onLoad(options) { this.setData({ type: options.type, signId: options.signId }); // 根据 type 渲染不同的签到组件 if (options.type === 'gsign') { this.initGestureCanvas(); // 初始化手势画布 } }参数说明:signId是后端返回的签到任务主键,学生提交签到时必须带上,否则后端不知道签的是哪一次。type决定前端渲染哪个组件,手势签到要初始化 canvas,位置签到要调wx.getLocation。
4.3 位置签到的权限申请与坐标获取
位置签到最容易翻车的地方是权限。小程序调wx.getLocation之前,必须在app.json里声明permission:
{ "permission": { "scope.userLocation": { "desc": "签到需要获取您的位置信息" } } }如果用户之前拒绝过,wx.getLocation会直接 fail。这时候要引导用户去wx.openSetting重新授权:
wx.getLocation({ type: 'gcj02', success: res => { // res.latitude, res.longitude 传给后端 }, fail: () => { wx.showModal({ title: '需要位置权限', content: '请在设置中开启位置权限后重试', success: modalRes => { if (modalRes.confirm) { wx.openSetting(); // 打开权限设置页 } } }); } });参数说明:type: 'gcj02'返回的是国测局坐标系,和微信内置地图一致。如果你后端存的是 GPS 原始坐标(wgs84),需要做一次转换,否则距离计算会偏几十到几百米。这是位置签到最大的坑,没有之一。
5. 避坑与常见问题排查:那些让我熬夜的报错
5.1 数据库连接报Public Key Retrieval is not allowed
现象:启动 Tomcat 时控制台刷Communications link failure,或者Public Key Retrieval is not allowed。
原因:MySQL 8.0 默认用caching_sha2_password认证插件,旧版 JDBC 驱动不兼容。
解决:在jdbc.url后面加allowPublicKeyRetrieval=true&useSSL=false,同时把mysql-connector-java升级到 8.0.x。如果还是不行,进 MySQL 执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';。
5.2 小程序请求后台报不在以下 request 合法域名列表中
现象:开发者工具里请求正常,真机预览时所有接口都失败。
原因:小程序正式环境要求 HTTPS 且域名备案,开发阶段没配。
解决:开发者工具里勾选「不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书」。真机调试时,手机和电脑连同一个 WiFi,BASE_URL用电脑局域网 IP,不要用localhost。
5.3 手势签到路径比对总是失败
现象:学生画的手势和老师设的一模一样,后端就是返回不匹配。
原因:前端传的路径字符串可能带了空格或分隔符不一致,比如老师存的是1,2,3,5,学生传的是1-2-3-5。
解决:统一分隔符,后端在比对前先replaceAll("[^0-9]", "")把非数字字符全去掉,再比。或者前端传之前就join('')成纯数字串。
5.4 位置签到距离计算偏差过大
现象:学生就在教室里,后端算出来距离 500 米。
原因:坐标系不统一。教师端存的是gcj02,学生端传的是wgs84,或者反过来。
解决:全链路统一用gcj02。如果教师端是从地图选点拿的坐标,确认一下地图 SDK 返回的坐标系。实在不行,在计算距离前加一个坐标转换函数,把wgs84转gcj02。
5.5 MyBatis 查询返回字段全为 null
现象:selectByExample查出来 List 有数据,但每个对象的属性都是 null。
原因:Mapper.xml里的resultMap和实体类字段名没对上,或者Base_Column_List里列名写错。
解决:检查resultMap的column和property是否一一对应,特别是下划线转驼峰的地方。比如数据库列class_id对应实体属性classId,resultMap里要写column="class_id" property="classId"。如果开了mapUnderscoreToCamelCase,可以省这一步,但得在mybatis-config.xml里设true。
6. 进阶:把签到数据用起来与二次开发建议
跑通之后,这份源码的价值不止于「能签到」。lsign、psign、gsign表里积累的签到记录,可以导出成 CSV 做平时成绩统计。我一般会加一个export接口,用 POI 写 Excel:
// 伪代码:导出某班级某课程的签到记录 public void exportSignRecords(HttpServletResponse response, int classId) { List<SignRecord> records = signService.getRecordsByClass(classId); XSSFWorkbook workbook = new XSSFWorkbook(); XSSFSheet 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); row.createCell(0).setCellValue(records.get(i).getName()); row.createCell(1).setCellValue(records.get(i).getSignTime()); row.createCell(2).setCellValue(records.get(i).getType()); } response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=sign_records.xlsx"); workbook.write(response.getOutputStream()); workbook.close(); }参数说明:classId用来过滤班级,SignRecord是一个 DTO,把四张表的数据 union 起来。Content-Disposition里的文件名不要用中文,否则某些浏览器会乱码,用sign_records.xlsx最稳。
二次开发建议:四种签到模式里,位置签到的防代签强度最高,但依赖 GPS,室内漂移大;手势签到体验好,但学生可以把路径截图发给别人。如果让我选,我会把位置签到和密码签到组合使用——先验位置再验密码,双重保险。源码里四种模式是独立的,你可以加一个combined类型,在APIController里串行调用两个校验方法。
还有一个容易忽略的点:签到的并发。如果 120 个学生同时点签到,数据库可能扛不住。常见做法是在APIService里加synchronized或者用 Redis 做分布式锁,但课程设计级别一般不用。如果你要压测,用 JMeter 开 200 个线程打doLsign接口,看看响应时间。我实测过,没加锁的情况下 MySQL 连接池设 20 个,200 并发会有部分超时。把连接池调到 50,超时消失,但 CPU 会飙。这是取舍。
从那以后我每次拿到 SSM 源码,都强制先跑一遍「建库 → 改配置 → 打接口 → 真机联调」四步,不跳过任何一步。希望这份拆解能帮你省下几个熬夜的晚上,顺利把签到模块跑起来。
本文还有配套的精品资源,点击获取