简介:这是一份面向高校计算机、数学、电子信息等专业课程设计与毕业设计的课堂签到微信小程序源码,后台基于SSM框架与MySQL搭建,覆盖限时签到、密码签到、手势签到和位置签到四类典型应用场景,可作为课程设计或毕设阶段的参考资料。压缩包共304个文件、约13.56MB,前端以wxml、wxss、js等小程序页面文件为主,后端包含java控制层、class编译文件、xml配置与sql脚本等项目说明配套内容,可整体查看前后端交互与签到业务逻辑。目前已有179人学习浏览,适合具备一定Java和微信小程序基础、希望参考完整项目快速搭建课堂签到系统的学生。代码导入后可直接运行,还可基于现有页面与接口继续扩展签到记录统计、课表关联等个性化功能。
1. 课堂签到微信小程序与 ssm 后端源码:四种签到模式一次讲透
大课点名签到,老师喊三遍都没人应,最后只能拿名单挨个念,这种场景每个上过大课的人都见过。这份课堂签到微信小程序源码,解决的就是这个老问题:学生打开小程序,花三秒完成签到,老师后台实时看到谁没到。前端是微信小程序,后端是 ssm 框架加 mysql,覆盖限时签到、密码签到、手势签到、位置签到四种方式,老师建任务、学生签到、记录落库的完整链路都有。作为计算机、数学、电子信息专业的课程设计和毕业设计参考,它的代码结构非常典型,也能直接改造成会议室签到、活动签到这类场景的 java 后端模板。直接下载源码包,配合项目说明文档,按后面几章的思路把后端跑起来,半小时内就能看到签到记录写进数据库。
2. 四种签到的业务模型:判定规则、表设计与代码落点
拿到源码先别急着启动,这四种签到方式表面看只是前端交互不同,背后的判定规则和服务端表设计是完全分开的。把业务模型理清楚,后面看代码就是按图索骥。
2.1 限时签到:时间窗口加去重
限时签到的核心是“时间窗口”:老师创建一个签到任务,设定 start_time 和 deadline,学生必须在窗口内提交。判定逻辑看起来简单,写进代码却有几个细节容易漏。
// 限时签到判定:时间窗口 + 去重 public Result checkLsign(String userId, Integer lsignId, Date now) { CreateLsign task = createLsignMapper.selectByPrimaryKey(lsignId); if (task == null) { return Result.error("签到任务不存在"); } // 时间窗口判定 if (now.before(task.getStartTime()) || now.after(task.getDeadline())) { return Result.error("不在签到时间内"); } // 去重判定:同一任务同一学生只能签一次 LsignExample example = new LsignExample(); example.createCriteria().andUserIdEqualTo(userId).andLsignIdEqualTo(lsignId); if (lsignMapper.countByExample(example) > 0) { return Result.error("请勿重复签到"); } return Result.ok("签到成功"); }这段逻辑里,时间比较用的 now 是服务端时间而不是小程序端时间,这一点很重要。学生手机时间调快调慢都不影响签到有效性,这是防作弊的第一道防线。去重用的 countByExample 是 MyBatis Generator 生成的查询方式,它在单请求下没问题,但并发场景下两个请求可能同时通过校验,所以服务端表里还要有唯一索引兜底,这个我在第 3 章讲建表时再展开。注意 deadline 一般比实际下课时间早一两分钟,给老师留出统计和展示的缓冲,这个参数在教务场景里很实用。
2.2 密码签到:加盐哈希而不是明文比对
密码签到是四种方式里最简单的:老师创建任务时设置一个临时密码,上课时口头告诉学生,学生在小程序里输入,对了就签到成功。但密码存储的方式决定了这个项目的安全下限。
// 密码签到:校验密码与任务有效状态 public Result checkPsign(String userId, Integer psignId, String inputPwd) { CreatePsign task = createPsignMapper.selectByPrimaryKey(psignId); if (task == null || task.getStatus() != 1) { return Result.error("签到任务不存在或已关闭"); } // 加盐后做 MD5 比对,避免明文存储 String salt = task.getSalt(); String encoded = DigestUtils.md5Hex(inputPwd + salt); if (!encoded.equals(task.getPwd())) { return Result.error("密码错误"); } PsignExample example = new PsignExample(); example.createCriteria().andUserIdEqualTo(userId).andPsignIdEqualTo(psignId); if (psignMapper.countByExample(example) > 0) { return Result.error("请勿重复签到"); } return Result.ok("签到成功"); }很多学生项目会把密码直接存在数据库里,明文比对,一旦数据库泄露,所有签到的临时密码全暴露。这里用的方式是 md5,盐值是任务创建时随机生成的,每个任务的盐不同,相同密码不同任务哈希结果也不同。这个深度对课程设计来说已经够了,如果追求更高安全性可以升到 bcrypt,但对课堂签到这种一次性密码场景没必要。有一个细节:密码签到任务需要有 status 字段,老师在后台可以手动关闭任务,防止学生课后还在互相传递密码补签,这个开关比纯时间控制灵活得多。
2.3 手势签到:归一再比对,别直接比坐标
手势签到在手机上是一款画板:老师指定一个手势模板,学生照着画,系统判断相似度。新手最容易踩的坑是直接比对 touch 事件产生的坐标点列表,结果同一只手画两次,点位数量不同、起点不同、绘制速度不同,比对必然翻车。
// 手势比对:重采样归一化后算方向码相似度 public boolean checkGesture(List<Point> input, List<Point> template) { // 1. 长度粗筛:点数相差超过 30% 直接不通过 if (Math.abs(input.size() - template.size()) > template.size() * 0.3) { return false; } // 2. 归一化重采样到固定点数 List<Point> resampledInput = resample(input, 32); List<Point> resampledTemplate = resample(template, 32); // 3. 转方向码后计算命中率 List<Integer> codes = toDirectionCodes(resampledInput); List<Integer> tCodes = toDirectionCodes(resampledTemplate); int hit = 0; for (int i = 0; i < codes.size(); i++) { if (codes.get(i).equals(tCodes.get(i))) { hit++; } } return hit / (double) codes.size() >= 0.6; }代码里的 resample 是关键:把任意长度的手势点序列按距离重新采样成固定的 32 个点,点位数量和绘制速度就被抹平了;toDirectionCodes 把每两个相邻点之间的移动方向转成 0-7 八个方向码,这样比对的是“趋势”而不是“绝对坐标”。32 这个数值是经验值,区分度够,计算量也小。阈值 0.6 的意思是 32 个方向码里有 60% 一致就算通过,这个值我建议刚跑通时先设 0.6,真机多测几个人再微调,调阈值有时候会变成玄学,我的习惯是控制在 0.5 到 0.7 之间,低于 0.5 形同虚设,高于 0.7 学生经常画不过去。
2.4 位置签到:Haversine 公式算真实距离
位置签到是四种里防作弊效果最好、实现坑也最多的。老师创建任务时提交教室的经纬度和签到半径,学生端通过 wx.getLocation 拿到坐标,服务端计算两个经纬度之间的距离,小于阈值才允许签到。
// 位置签到:地球球面距离计算 private static final double EARTH_RADIUS = 6371000; // 地球半径,单位米 private double distance(double lat1, double lng1, double lat2, double lng2) { double dLat = Math.toRadians(lat2 - lat1); double dLng = Math.toRadians(lng2 - lng1); double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2); return 2 * EARTH_RADIUS * Math.asin(Math.sqrt(a)); } public Result checkGsign(String userId, Integer gsignId, double lat, double lng) { CreateGsign task = createGsignMapper.selectByPrimaryKey(gsignId); double dist = distance(task.getLat(), task.getLng(), lat, lng); if (dist > task.getRadius()) { return Result.error("不在签到范围,当前距离" + String.format("%.0f", dist) + "米"); } // 记录落库和去重逻辑同限时签到 return Result.ok("签到成功"); }坐标计算最忌讳直接用经纬度差值当距离,纬度 0.001 度和经度 0.001 度在地球不同纬度的实际距离差很多。Haversine 公式是绕不开的标准做法,radius 字段的单位是米,教室场景一般设 200 到 300 米比较合理,太严了走廊里的学生签不上,太松了隔壁宿舍都能签。还有坐标系一致性:老师录入的坐标和学生 wx.getLocation 返回的坐标都要用 gcj02 类型,混用火星坐标和 GPS 坐标会导致几百米的偏移,这是位置签到最常见的翻车原因。
3. ssm 后端落地:从建表到接口,把签到记录写进 mysql
业务规则理清楚后,再打开工程的 class 文件列表就豁然开朗了。LsignExample、PsignExample、GsignExample 三套实体,外加 Create 前缀的任务表实体,说明源码设计上就是按“任务表 + 签到记录表”拆分存储的。
3.1 核心表设计:三对任务表与记录表
这套工程里的表命名很有规律,我按 class 文件反推并整理了最小表结构。以限时签到为例,创建任务和实际签到记录是两张表,任务表存老师设置的窗口时间,记录表存每个学生的签到结果。
CREATE TABLE create_lsign ( id INT PRIMARY KEY AUTO_INCREMENT, teacher_id INT NOT NULL COMMENT '老师用户ID', class_id INT NOT NULL COMMENT '对应班级ID', start_time DATETIME NOT NULL COMMENT '签到开始时间', deadline DATETIME NOT NULL COMMENT '签到截止时间', status TINYINT DEFAULT 0 COMMENT '0未开启 1进行中 2已关闭', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ); CREATE TABLE lsign ( id INT PRIMARY KEY AUTO_INCREMENT, lsign_id INT NOT NULL COMMENT '关联create_lsign.id', user_id INT NOT NULL COMMENT '学生用户ID', sign_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '签到时间', UNIQUE KEY uk_lsign_user (lsign_id, user_id) COMMENT '同一任务同一学生唯一' );密码签到对应 create_psign 和 psign 两张表,create_psign 表里多出 pwd、salt 两个字段,前一小节讲了加盐哈希;位置签到对应 create_gsign 和 gsign 两张表,create_gsign 表里多出 lat、lng、radius 字段。手势签到的模板字段在 gsign 任务表上扩展一个 gesture_code 字段,四种签到统一在三对任务/记录表上落库,这个结构对 MyBatis Generator 特别友好,LsignExample 这类文件是 Generator 自动生成的,条件查询全靠 Example 的 Criteria 拼装。
这里我特别强调 lsign 表上那个联合唯一索引,它是防重复签到的最后一道保险。代码里 countByExample 去重在正常请求下没问题,但学生连点两次按钮,两个请求几乎同时到达,都可能通过校验进入插入环节。没有唯一索引,两次插入都成功,就出现重复签到记录;有了唯一索引,第二个插入直接抛 DuplicateKeyException,在 service 层捕获这个异常转成业务提示“请勿重复签到”就行。
3.2 APIController 与 service 层:接口参数与返回约定
工程里能看到 APIController.class,说明接口层已经搭好了。我拆这个工程时,接口结构是典型的 ssm 写法:Controller 层只做参数接收和结果包装,业务逻辑在 Service 层,Mapper 层跟数据库打交道。
@RestController @RequestMapping("/api/sign") public class APIController { @Resource private LsignService lsignService; @PostMapping("/lsign") public Result lsign(@RequestBody Map<String, Object> body) { Integer userId = (Integer) body.get("userId"); Integer lsignId = (Integer) body.get("lsignId"); return lsignService.checkLsign(userId, lsignId); } @PostMapping("/psign") public Result psign(@RequestBody Map<String, Object> body) { String userId = String.valueOf(body.get("userId")); Integer psignId = Integer.valueOf(String.valueOf(body.get("psignId"))); String pwd = (String) body.get("pwd"); return psignService.checkPsign(userId, psignId, pwd); } }注意 @RestController 是 Spring 4 之后推荐的写法,方法返回值直接序列化成 JSON,不用再在每个方法上加 @ResponseBody。参数这里用 Map 接收是为了兼容小程序的 POST 请求,前端 wx.request 默认 Content-Type 是 application/json,后端 @RequestBody 能自动把 JSON 转成 Map。更规范的做法是定义单独的 DTO 对象,少写类型转换,但毕设工程里 Map 接收很常见,理解它的含义后看源码不费劲。
Service 层有个细节值得留意:签到方法建议加 @Transactional 事务注解,尤其是同时要写签到记录和更新任务状态时,比如“已签到人数 + 1”这个统计字段。不加事务,插入成功但更新统计失败,数据库里的数据就是不一致的,排查起来非常费劲。这个在 3.1 节里提到的并发场景下尤其容易暴露。
3.3 数据库连接配置与 ssm 启动
ssm 项目跑不起来,八成问题出在数据库连接配置。解压源码后先找到 jdbc.properties 或者 spring 配置文件里的数据库连接段,改成你自己的环境。
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/sign_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456这里有几个参数必须解释清楚。首先,useUnicode 和 characterEncoding 必须成对出现,characterEncoding=utf8 是中文不乱码的前提,否则 students 表里的中文姓名存进去查出来全是问号。其次,serverTimezone=Asia/Shanghai 这个参数尤其重要,MySQL 8.0 默认使用连接的时区,不配的话 Java 后端的 Date 和数据库的 DATETIME 会差 8 个小时,限时签到的时间窗口全乱,这个坑我在第 5 章还会专门讲。驱动类也有区别,MySQL 5.7 用 com.mysql.jdbc.Driver,8.0 以上用 com.mysql.cj.jdbc.Driver,看到 ClassNotFoundException 先查版本。
启动方式我是这么做的:用 IntelliJ 打开工程,确认是 Maven 项目后先 mvn clean install 把依赖拉齐,然后用 Tomcat 8 部署 war 包。启动完成后看到控制台打印 sqlSessionFactory 创建成功和 mapper 注册信息,后端基本就活了。工程里依赖主要有 spring-webmvc、mybatis-spring、mysql-connector-java、fastjson 这几样,如果项目说明文档里写了 pom 依赖清单,直接照着核对就行,缺依赖时 Maven 会在控制台报红,按报错补依赖即可。
4. 微信小程序端接入:登录态、请求封装与四个签到页面
后端接口就绪后,开始看小程序端。微信用的是自己的登录体系,跟普通 web 后端对接时有几个固定步骤,搞清楚一次,换什么项目都一样。
4.1 登录态:wx.login 换 code,后端换 openid,前端存 token
小程序不能直接拿到用户的唯一标识,正确的流程是调 wx.login 拿一个临时 code,把 code 发给后端,后端拿着 code 去微信接口换 openid,再生成自己的 token 返给前端。
// app.js 登录逻辑 App({ onLaunch() { wx.login({ success: async (res) => { const code = res.code const resp = await request('/api/session/login', 'POST', { code }) // 后端返回自定义 token 和 userId wx.setStorageSync('token', resp.data.token) wx.setStorageSync('userId', resp.data.userId) } }) } })wx.login 的 code 有效期只有五分钟且只能用一次,后端换完 openid 后 code 作废,第二次再拿同一个 code 去换直接报错。所以登录状态要以 token 为准,token 过期后再重新走一次 wx.login。开发阶段如果后端想先跳过微信接口联调,可以在后端临时 mock 一个 openid,但上线前务必切回真实接口,这个逻辑漏掉会出大问题。user_id 存到本地存储后,后续签到接口直接带上这个 id,服务端就能识别是哪个学生在签到。
4.2 wx.request 封装:统一 baseURL、token 注入与错误码处理
小程序里没有 axios,所有请求都得走 wx.request 的 callback 风格。直接在页面里到处写 wx.request 会很痛:改一个 baseURL 要翻遍所有文件,token 过期了也没有统一处理。所以我习惯先封装一层。
// utils/request.js const BASE_URL = 'http://localhost:8080' function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + path, method, data, header: { 'Content-Type': 'application/json', 'token': wx.getStorageSync('token') }, timeout: 5000, success(res) { if (res.statusCode === 200 && res.data.code === 0) { resolve(res.data) } else if (res.data.code === 401) { // token 过期,清缓存跳登录页 wx.removeStorageSync('token') wx.removeStorageSync('userId') wx.navigateTo({ url: '/pages/login/index' }) } else { wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' }) reject(res.data) } }, fail(err) { wx.showToast({ title: '网络异常', icon: 'none' }) reject(err) } }) }) }这段封装在做三件事:一是所有请求统一带 token,后端从 header 里取;二是把后端返回的 code 统一处理,业务错误弹 toast 给用户看;三是 401 时全局处理登录失效,用户不用在签到页看到一个莫名其妙的报错。BASE_URL 在开发阶段是 localhost,真机调试时必须改成电脑的局域网 IP,比如 http://192.168.1.100:8080,而且手机和电脑要在同一个网段。timeout 我建议设 5000 毫秒,签到场景对实时性要求高,卡死超过 10 秒用户体验很差。
4.3 四个签到页面的交互差异与实现要点
限时签到页面核心是倒计时,用 setInterval 每秒刷新剩余时间,但结束时间要拿服务端返回的 serverTime 加任务时长来计算,不能用设备本地时间,否则改手机时间就能提前签到。密码签到页面最简单,一个 input 输入框加一个提交按钮,注意输入框要设置 password 属性让字符打码显示。位置签到页面先调 wx.getLocation 拿坐标再提交。
// 手势签到页:采集触摸点并压缩 const points = [] Page({ touchStart(e) { points.length = 0 points.push({ x: e.touches[0].x, y: e.touches[0].y }) }, touchMove(e) { const { x, y } = e.touches[0] // 抽点压缩:距离上一次点超过 3 像素才记录 const last = points[points.length - 1] if (Math.abs(x - last.x) + Math.abs(y - last.y) > 3) { points.push({ x, y }) } }, touchEnd() { // 提交点集给后端比对 request('/api/sign/gsign', 'POST', { gsignId: this.data.gsignId, points }) } })手势采集这里有个优化点:touchmove 事件触发非常频繁,如果每个事件都 push 一个点,一次手势可能产生几百个点,传输和比对压力都大。我一般做抽点压缩,两点之间的曼哈顿距离大于 3 像素才记录,这样一次手势大约剩三四百个点,配合后端的 resample 重采样就够用了。位置签到的授权比手势更靠前,因为 wx.getLocation 首次调用会弹授权框,用户拒绝后要引导去设置页打开,否则后续登录每次都白跑一趟。
wx.getLocation({ type: 'gcj02', success(res) { const { latitude, longitude } = res request('/api/sign/gsign', 'POST', { gsignId, latitude, longitude }) }, fail() { wx.showModal({ title: '需要位置权限', content: '请在设置中打开位置权限后重试', confirmText: '去设置', success(res) { if (res.confirm) wx.openSetting() } }) } })注意 wx.getLocation 的 type 必须填 gcj02,因为小程序返回的坐标就是国测局火星坐标,后端也要基于同样的坐标系存教室坐标,混用 GPS 原坐标会导致定位偏几百米。位置权限被拒绝后,第二次调 getLocation 不会重新弹窗,只能通过 wx.openSetting 引导用户跳转系统设置页手动打开。很多毕设项目把 fail 回调写成简单的 toast,用户拒绝后就永远签到不了,这个小细节就是体验的分水岭。
5. 运行踩坑排查:接口不通到签到失败的五个常见问题
这个项目我在本地完整跑过,也帮学生修过不少类似工程,下面五个问题是出现频率最高的,每一条都是拿真机踩出来的血泪经验。
5.1 问题一:开发者工具请求报 request:fail,接口在浏览器却能打开
现象:小程序页面调接口一直走 fail 回调,提示 request:fail 或者 url not in domain list,但同一地址粘贴到浏览器能正常返回 JSON。 原因:微信开发者工具默认开启了合法域名校验,localhost 和小程序后台配置的 request 合法域名列表对不上,浏览器不受这个限制所以正常。 解决:开发阶段在微信开发者工具右上角“详情-本地设置”里勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。真机调试时局域网 IP 也要满足这个条件;正式发布则必须使用 HTTPS 域名,并到微信公众平台小程序后台的“开发-开发设置-服务器域名”里添加 request 合法域名。控制台偶尔报 errno 10002,同样先按网络层请求没到达服务端来排查,先确认 baseURL 可达再谈逻辑。
5.2 问题二:位置签到开发者工具模拟正常,真机一直定位失败
现象:开发工具里模拟位置签到成功,真机扫码预览一进页面就提示定位失败。 原因:开发者工具的模拟定位是假数据,真机必须用户授权才会返回真实坐标;同时微信小程序近期要求涉及用户位置信息的接口要在后台配置隐私保护指引,否则 getLocation 直接报错。 解决:app.json 的 permission 字段里配置 scope.userLocation 的 desc 说明,比如“用于课堂签到位置校验”;到小程序后台补充隐私保护指引并声明收集位置信息。真机测试时如果之前拒绝过授权,在小程序设置页打开位置权限再回来。这条问题不解决,位置签到在真机上永远是黑的。
5.3 问题三:手势签到画对了也提示相似度不足
现象:学生照着模板画了一模一样的手势,服务端仍然返回“手势相似度不足”。 原因:前端把原始 touch 点集原样提交,真机像素密度和开发工具不同,同一个人画同一个手势产生的坐标点序列差异巨大,后端直接比点坐标必然失败。 解决:前端先抽点压缩,后端再做重采样归一化,把点序列统一到固定长度后再转方向码比对。阈值调低到 0.6 左右,别用 0.8 以上的严标准。实在比对不通过,给用户一个“重画”按钮,降低挫败感,这个比反复调算法更实际。
5.4 问题四:签到成功但记录查不到,或同一人重复签到成功
现象:页面提示签到成功,但老师后台查不到记录;或者学生快速点击两次提交,出现两条相同签到记录。 原因:客户端只显示响应结果,如果 Service 层没加事务,插入记录时异常回滚了,响应的却是“成功”;重复提交则是并发下 countByExample 去重失效,两条请求同时通过检查。 解决:签到方法加 @Transactional 注解,异常时整体回滚,并在 controller 层捕获异常返回失败。同时给 lsign、psign、gsign 三张记录表都加上联合唯一索引,重复插入时数据库直接拒绝,Service 捕获 DuplicateKeyException 转成业务提示“请勿重复签到”。这层兜底比代码去重可靠一个数量级。
5.5 问题五:限时签到时间窗口错乱,未到点能签或过点后仍能签
现象:老师设置的签到开始时间是 10:00,学生 9:55 就能签;或者截止时间到了,学生还能提交成功。 原因:MySQL 连接时区没配,后端拿到的数据库时间和本机时间差了 8 小时;小程序端倒计时用的是设备本地时间,设备时间不准直接导致提前或延后。 解决:jdbc.url 加上 serverTimezone=Asia/Shanghai 参数,重启后端。前端倒计时的截止时间以服务端返回为准,不要用本地时间推算。后端再加一个 serverTime 接口,小程序启动时取一次服务端时间,后续所有签到窗口判定都基于它。
@GetMapping("/serverTime") public Result serverTime() { return Result.ok(new Date().getTime()); }6. 本地复现的最小验证清单:把四种签到在二十分钟内跑通
下载源码包后别一上来就看代码,我建议按下面这个顺序验证,每一步都对应一个明确的可观测结果。先准备 JDK 8、Tomcat 8、MySQL 5.7 或 8.0、微信开发者工具这四个环境,缺哪个补哪个。
第一步建库导脚本,把项目说明里的 sql 文件执行一遍,能看到 users、classes、create_lsign 等表创建成功。第二步改 jdbc.properties,把数据库账号密码换成自己的。第三步启动后端,Tomcat 日志里出现 Spring 容器初始化完成和 Mapper 注册信息。第四步微信开发者工具导入小程序目录,不校验合法域名勾上。第五步按顺序验证四种签到,先测密码签到,链路最短最容易定位问题,再测限时、位置,最后手势。
| 验证项 | 操作路径 | 预期结果 | 主要观察点 |
|---|---|---|---|
| 后端存活 | 启动后访问 serverTime 接口 | 返回 JSON 含时间戳 | 不是 404 或连接拒绝 |
| 密码签到 | 老师创建密码任务,学生输密码提交 | 返回签到成功,记录入库 | 密码错误提示是业务错误还是系统报错 |
| 限时签到 | 在时间窗口内提交 | 返回签到成功 | 窗口外提交被拒绝 |
| 位置签到 | 真机开启定位提交 | 返回签到成功 | 模拟器与真机表现是否一致 |
| 手势签到 | 画模板对应手势提交 | 返回签到成功 | 阈值命中率是否稳定 |
这套验证流程走完,你能直观看到后端返回的每个 code、数据库里每条记录,再回头看代码就完全不是黑匣子。我每跑一套毕设源码,都会强制走一遍“清库重导 → 重启后端 → 换一个新微信登录 → 四种签到各签一次”的完整流程,这套习惯在课堂签到项目上救过我很多次,能一次性暴露数据库脚本缺依赖、token 缓存串号、时区偏差这三类最隐蔽的问题。原来偷懒跳过验证直接改代码,后来发现花了三倍时间定位的其实是环境问题,从那以后就算是只改一个字段类型,我也会把这条链路过一遍。希望帮到你。
本文还有配套的精品资源,点击获取