简介这份资源是一套完整可运行的课堂签到微信小程序源码及项目说明后台基于SSM框架与MySQL开发面向计算机、数学、电子信息等专业学生可作为课程设计、期末大作业或毕业设计参考。项目覆盖限时签到、密码签到、手势签到、位置签到等多种场景适合需要快速搭建签到系统或学习前后端联调的开发者。压缩包共304个文件包含Java源码与class文件、XML配置、JAR依赖库、微信小程序前端WXML/WXSS/JS文件以及SQL、属性文件等整体大小约13.56MB目录结构完整便于按模块阅读与二次开发。目前已有179人学习下载资源内含项目全部源码与说明文档可直接导入运行并可根据自身需求扩展功能适合具备一定Java基础、愿意深入研读代码的读者作为实战参考资料。1. 课堂签到微信小程序为什么核心难点在签到判定而不在界面课堂签到微信小程序并不复杂但很多第一次做的人会在“限时签到、密码签到、手势签到、位置签到”四种方式一起上线时翻车。不是界面难画而是这四种方式的后台校验逻辑彼此独立时间、密码、坐标、手势序列混在一起任何一个环节判断不严谨学生就能找到绕过签到的办法。这个项目适合两类人一类是正在做Java课程设计或者毕业设计的学生需要一套SSM MySQL的后台来撑住“有服务端、有数据库、有完整业务”的评分点另一类是已经写过小程序、想搞明白签到类业务边界和参数坑的开发者。如果只是想画个界面点个按钮那这个标题的价值不大如果想知道一次签到为什么会被判定为迟到、为什么在教室门口签不到这篇文章把落地路径讲清楚。2. 先看清项目全貌SSM后台、MySQL表结构与四种签到的业务边界2.1 后台为什么选SSMMySQL而不是Spring Boot或云开发“课堂签到微信小程序源码项目说明”这个标题本身把技术栈写死在名字里后台是SSM框架数据用MySQL。SSM指Spring、Spring MVC、MyBatis三个框架的组合是Java Web课程设计里的常青树。对比Spring BootSSM要手动写XML配置显得烦琐但只要把spring-mvc.xml、spring-mybatis.xml、web.xml三个配置文件理顺后面写业务和Boot没什么区别。选择SSM的现实理由往往是“评分标准要求”或者“教程资料多”。云开发更省事但后台逻辑全部跑在小程序云函数里答辩时拿不出独立的Java Web工程很容易被追问到下不来台。MySQL也是同理用SQLite或者内存存储能跑却没有“数据库设计”这个得分点。这个项目里签到记录、签到任务、用户表之间存在明确的关联关系用MySQL设计三张表就能把关系型数据库的约束、联合唯一键、索引都用上这正好是项目说明里最值的加分项。四种签到方式对应四种授课场景限时签到用于常规课堂验证“人到没到”密码签到适合考前随机点名教师现场念密码手势签到偏向低年级趣味性位置签到用于必须到指定场地上课的实验课。后台用sign_type字段区分互不复用逻辑这是项目结构清楚的关键。如果偷懒把密码签到和手势签到共用一套校验代码发布和校验都会变得很难改。2.2 核心表结构签到任务表、签到记录表与用户表后台不管怎么封装修饰最终数据都在三张表里转用户表存教师和学生签到任务表存发布出来的签到签到记录表存“谁在什么时间用什么方式签到了”。下面给出建表语句可以直接粘贴到Navicat或者MySQL命令行里执行。-- 用户表教师和学生共用一张表用role区分 CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, username VARCHAR(32) NOT NULL UNIQUE COMMENT 登录名教师工号或学号, password VARCHAR(64) NOT NULL COMMENT 登录密码建议BCrypt加密存储, real_name VARCHAR(16) NOT NULL COMMENT 姓名签到列表要展示, role TINYINT NOT NULL DEFAULT 2 COMMENT 角色1教师2学生, created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;这种单表角色区分优点是登录校验简单一次查询就知道身份缺点是学生和教师字段混在一起后续要扩展职称、班级时就得加字段这个尺度对课程设计来说刚刚好。密码列长度给64是为了兼容BCrypt输出长度如果用MD5也放得下但项目说明里最好写清楚你用的是哪种别让评分老师产生歧义。-- 签到任务表一次课堂签到就是一条任务记录 CREATE TABLE sign_task ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 签到任务ID, title VARCHAR(64) NOT NULL COMMENT 签到标题例如“计算机网络 第3次课”, sign_type TINYINT NOT NULL COMMENT 签到方式1限时2密码3手势4位置, teacher_id INT NOT NULL COMMENT 发布签到的教师ID关联sys_user.id, start_time DATETIME NOT NULL COMMENT 签到开始时间, end_time DATETIME NOT NULL COMMENT 签到截止时间, password_hash VARCHAR(64) NULL COMMENT 密码签到或手势签到的哈希值限时/位置签到时为空, location_name VARCHAR(64) NULL COMMENT 位置签到时显示的地点名称, latitude DECIMAL(10,6) NULL COMMENT 位置签到的参考纬度, longitude DECIMAL(10,6) NULL COMMENT 位置签到的参考经度, allow_distance INT NOT NULL DEFAULT 200 COMMENT 位置签到的允许误差半径单位米, status TINYINT NOT NULL DEFAULT 1 COMMENT 任务状态1进行中2已结束3已取消, created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, KEY idx_teacher_time (teacher_id, start_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT签到任务表;这张表是四种签到方式的公共载体。sign_type 决定校验分支password_hash 在密码签到里保存密码的哈希在手势签到里保存手势点序字符串的哈希在限时和位置签到里为空。allow_distance 只对位置签到有意义默认200米的经验值后面避坑章节细说。索引建在(teacher_id, start_time)上是为了教师首页展示“我发布过的签到列表”时不产生全表扫描。-- 签到记录表学生每次签到写入一条记录 CREATE TABLE sign_record ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 记录ID, task_id INT NOT NULL COMMENT 签到任务ID关联sign_task.id, student_id INT NOT NULL COMMENT 学生ID关联sys_user.id, sign_time DATETIME NOT NULL COMMENT 实际签到时间, latitude DECIMAL(10,6) NULL COMMENT 签到时学生所在纬度位置签到时有效, longitude DECIMAL(10,6) NULL COMMENT 签到时学生所在经度, extra VARCHAR(255) NULL COMMENT 冗余字段可存签到时网络类型或客户端版本, status TINYINT NOT NULL DEFAULT 1 COMMENT 签到状态1正常2迟到3异常, UNIQUE KEY uk_task_student (task_id, student_id), KEY idx_student_time (student_id, sign_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT签到记录表;签到记录表里最重要的约束是uk_task_student联合唯一键它保证同一个学生对同一个签到任务只能签一次防止用两个手机号或者清缓存重登反复签到。status列在写入时由后台根据是否在end_time之前决定迟到的判断在写入那一刻完成不要在查询时才临时算否则统计出勤率时容易因为时区不一致出现偏差。2.3 源码包里的前后端目录小程序、后台和项目说明是如何对上的从文件名可以看出压缩包里除了源码还带了“项目说明”这是课程设计里很关键的交付物。常见的目录骨架是这样组织的class-sign/ ├── backend/ # SSM后台工程 │ ├── pom.xml │ ├── src/main/java/com/classroom/sign/ │ │ ├── controller/ # Spring MVC控制器 │ │ ├── service/ # 业务逻辑 │ │ ├── mapper/ # MyBatis数据访问接口 │ │ └── model/ # 实体类 │ └── src/main/resources/ │ ├── jdbc.properties │ ├── spring-mvc.xml │ ├── spring-mybatis.xml │ └── mapper/ # SQL映射XML ├── miniprogram/ # 微信小程序前端 │ ├── app.js │ ├── app.json │ └── pages/ │ ├── teacher/publish/ # 教师发布签到 │ ├── student/sign/ # 学生签到 │ └── login/ └── 项目说明.md后台工程推荐用Maven管理依赖Java代码按controller/service/mapper/model四层组织。小程序端用原生微信小程序就够了不需要引入uni-app或者Taro因为这个项目前后端交互就是几个wx.request原生的代码量最少答辩时也最好讲。项目说明文档建议写成“运行环境 - 数据库初始化 - 后台启动 - 小程序导入”的顺序把MySQL的初始化SQL单独放在一个sql目录里而不是藏在项目说明的某个段落这样别人拿到源码包能按步骤跑起来。3. 把后台接口跑通SSM核心代码与四种签到校验的判定逻辑3.1 配置数据源和MyBatisjdbc.properties与spring-mybatis.xmlSSM最劝退新手的不是Java代码而是XML配置。数据源配置写在jdbc.properties里下面这组在MySQL 8下验证过直接放在src/main/resources下即可。jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/class_sign?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password你的数据库密码MySQL 5.7和8的驱动类名不一样8必须用com.mysql.cj.jdbc.Driver用旧的com.mysql.jdbc.Driver会警告但不至于立即失败不过建议直接换新。URL里的参数别删characterEncoding保证中文不乱码serverTimezoneAsia/Shanghai告诉驱动用东八区时间否则后端读取DATETIME时会差8个小时useSSLfalse和allowPublicKeyRetrievaltrue是MySQL 8本地连接时的常见开关前者关掉SSL握手后者允许驱动在第一次连接时请求公钥不写全很容易遇到“Public Key Retrieval is not allowed”。然后写spring-mybatis.xml把数据源、SqlSessionFactory和Mapper扫描配置串起来?xml version1.0 encodingUTF-8? beans xmlnshttp://www.springframework.org/schema/beans xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd context:property-placeholder locationclasspath:jdbc.properties/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ property nameinitialSize value5/ property namemaxActive value20/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.classroom.sign.model/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.classroom.sign.mapper/ /bean /beansDruid连接池的initialSize和maxActive按课程设计的量级配5和20足够不用追求大参数。sqlSessionFactory里的mapperLocations指向resources/mapper目录如果你把SQL写在mapper接口注解上这里会扫不到建议统一用XML排查SQL时也方便复制到Navicat执行。3.2 让后台先被小程序访问到Tomcat端口与开发者工具设置用IDEA或者Eclipse把SSM工程跑起来之前建议先确认Tomcat端口。默认8080如果被占用小程序端baseUrl也要跟着改。常见做法是把后台工程以war包方式部署到Tomcat的webapps目录或者直接在IDE里配置Tomcat运行。注意小程序开发者工具默认会校验request的合法域名本地联调时后台是http://localhost:8080不满足正式域名的https要求。开发阶段要在“详情-本地设置”里勾选不校验合法域名否则所有wx.request都会直接进fail回调。这一步不花代码时间但能省掉大量“为什么请求发不出去”的排查。真机预览时baseUrl不能填localhost要填电脑在局域网里的IP例如http://192.168.1.101:8080并且手机和电脑要在同一个网络下。3.3 发布签到任务的后端接口Controller与Service实现教师端发布签到的入口是/api/task/publish。Controller层只做参数收口不写业务。RestController RequestMapping(/api/task) public class SignTaskController { Autowired private SignTaskService signTaskService; PostMapping(/publish) public Result publish(RequestBody SignTask task, RequestParam Integer teacherId) { if (task.getSignType() null || task.getSignType() 1 || task.getSignType() 4) { return Result.error(签到方式必须是1到4); } if (task.getStartTime() null || task.getEndTime() null) { return Result.error(签到起止时间不能为空); } if (task.getEndTime().before(task.getStartTime())) { return Result.error(结束时间不能早于开始时间); } return signTaskService.publish(task, teacherId); } }发布接口的校验点有三个签到方式取值范围、起止时间非空、结束时间在开始时间之后。Controller里不直接查库把组合校验放到Service里做是为了保持Controller薄后面加“同一位教师同一时间不能重复发签到”这类规则时不用改动接口层。teacherId这里为了阅读方便作为参数传入实际项目应从Session或Token中解析当前登录用户不能信任前端传的身份。Service实现里关键点是根据signType初始化扩展字段Service public class SignTaskServiceImpl implements SignTaskService { Autowired private SignTaskMapper signTaskMapper; Override public Result publish(SignTask task, Integer teacherId) { task.setTeacherId(teacherId); task.setStatus(1); switch (task.getSignType()) { case 2: // 密码签到后台生成6位随机密码存入哈希值 String code String.format(%06d, new Random().nextInt(999999)); task.setPasswordHash(md5(code)); break; case 3: // 手势签到前端提交手势点序后台只负责存哈希 if (task.getPasswordHash() null || task.getPasswordHash().isEmpty()) { return Result.error(手势签到必须先绘制手势); } break; case 4: // 位置签到校验教师坐标是否填写 if (task.getLatitude() null || task.getLongitude() null) { return Result.error(位置签到必须提交经纬度); } break; default: break; } signTaskMapper.insert(task); return Result.ok(发布成功, task.getId()); } }这里有一个容易理解错的点密码签到生成的明文密码只有在发布响应里返回一次数据库只存哈希。学生在小程序端输入明文后台做同样的MD5后比对。手势签到的password_hash字段在此时是前端上传的点序字符串哈希后台不反解手势内容所以同一个手势换一种连法也能通过校验这在前端交互章节细说。3.4 校验签到的核心逻辑限时、密码、位置、手势四种判定学生端提交签到的接口是/api/sign/doSign这是整个后台最值得写的部分。一次请求进来后台按“先时间、后凭据”的顺序做四段校验。Service public class SignServiceImpl implements SignService { Override public Result doSign(SignRequest req) { SignTask task signTaskMapper.selectById(req.getTaskId()); if (task null || task.getStatus() ! 1) { return Result.error(签到任务不存在或已结束); } Date now new Date(); // 第一步限时判定任何签到方式都必须在时间窗口内 if (now.before(task.getStartTime())) { return Result.error(签到还没开始); } if (now.after(task.getEndTime())) { return Result.error(签到已截止); } // 第二步按方式校验凭据 switch (task.getSignType()) { case 2: return checkPassword(req, task); case 3: return checkGesture(req, task); case 4: return checkLocation(req, task); default: break; } // 限时签到走到这里直接写入记录 return writeRecord(req, task, 1); } }时间判定放在最前面因为四种签到方式的共同底线是“必须在规定时间窗口内完成”。这里用的是后台服务器时间new Date()不是客户端传来的时间。小程序端即使被改了手机时间也影响不到这个判断这也是签到项目必须有的基本安全设计。密码签到的校验逻辑private Result checkPassword(SignRequest req, SignTask task) { String input req.getPassword(); if (input null || !md5(input).equals(task.getPasswordHash())) { return Result.error(密码错误); } return writeRecord(req, task, 1); }密码校验没有防爆破策略如果教师发布的是6位纯数字密码理论上1万种组合可以被脚本很快试完。课程设计通常不追求这个安全级别但可以在代码注释里写明生产环境要加连续错误次数锁定。位置签到的校验逻辑重点在于经纬度距离计算private Result checkLocation(SignRequest req, SignTask task) { if (req.getLatitude() null || req.getLongitude() null) { return Result.error(未获取到定位信息); } double distance distance(task.getLatitude().doubleValue(), task.getLongitude().doubleValue(), req.getLatitude().doubleValue(), req.getLongitude().doubleValue()); if (distance task.getAllowDistance()) { return Result.error(当前位置不在签到范围内); } SignRecord record new SignRecord(); record.setLatitude(req.getLatitude()); record.setLongitude(req.getLongitude()); return writeRecord(req, task, 1); } private static double distance(double lat1, double lng1, double lat2, double lng2) { double r 6371000; double rad Math.PI / 180.0; double dLat (lat2 - lat1) * rad; double dLng (lng2 - lng1) * rad; double a Math.sin(dLat / 2) * Math.sin(dLat / 2) Math.cos(lat1 * rad) * Math.cos(lat2 * rad) * Math.sin(dLng / 2) * Math.sin(dLng / 2); return 2 * r * Math.asin(Math.sqrt(a)); }这段用的是Haversine公式地球半径取6371000米计算两点球面距离。需要注意的点是如果两类坐标一个取WGS-84一个取GCJ-02算出来的距离会偏移几百米所以后台存储和前端上报必须约定成同一种坐标系。小程序端wx.getLocation返回的gcj02坐标直接透传即可不要自己转一次再传。手势签到的校验其实只校验点序哈希private Result checkGesture(SignRequest req, SignTask task) { if (req.getGesture() null || req.getGesture().isEmpty()) { return Result.error(手势序列为空); } if (md5(req.getGesture()).equals(task.getPasswordHash())) { return writeRecord(req, task, 1); } return Result.error(手势不正确); }手势序列的生成规则由前端控制后台只需要知道它是“1-3-5-7”这样的数字串。这个设计相对安全因为数据库里不存明文手势路径即使有人拿到后台数据也无法还原出手势图案。writeRecord里做重复签到拦截利用sign_record表上的联合唯一键来兜底private Result writeRecord(SignRequest req, SignTask task, int status) { SignRecord record new SignRecord(); record.setTaskId(task.getId()); record.setStudentId(req.getStudentId()); record.setSignTime(new Date()); record.setExtra(req.getExtra()); try { signRecordMapper.insert(record); return Result.ok(签到成功, record); } catch (DuplicateKeyException e) { return Result.error(重复签到); } }利用数据库唯一键而不是先查后插是为了避免并发场景下两个请求同时查到“没有记录”然后各自插入。DuplicateKeyException是MyBatis插入违反唯一约束时抛出的异常捕获它比捕获通用Exception更精确也方便前端展示明确文案。这里的studentId实际也应从登录态获取接口拒绝接受前端传值防止学生伪造身份。4. 小程序端四种签到方式的实现从页面到提交4.1 统一请求封装为四个签到页面准备一个带token的wx.request签到页面多四种提交方式和任务列表都要调后台。如果每个页面都写一次wx.request后面要改baseUrl或者加统一错误提示就会很痛苦。我一般先在utils/request.js里封装一个Promise风格的请求方法把公共逻辑收敛起来。// utils/request.js const baseUrl http://localhost:8080 function request(url, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: baseUrl url, method, data, header: { Content-Type: application/json, Authorization: wx.getStorageSync(token) || }, success: (res) { if (res.data.code 401) { wx.redirectTo({ url: /pages/login/login }) reject(res.data) return } resolve(res.data) }, fail: reject }) }) } module.exports { request }把baseUrl统一写在这里开发时指向局域网IP加端口发布前改线上地址一处生效。Authorization从Storage里取是因为SSM后台往往用拦截器校验登录状态前端只在请求层统一带token避免每个签到页面重复处理。页面里调用时只需要const { request } require(../../utils/request)然后request(/api/sign/doSign, POST, data)返回的是一个Promise用async/await或者.then都能接。4.2 限时签到的倒计时与时间校验小程序端限时签到页面最直观的交互是一个倒计时面板。后端已经校验了真正的时间窗口前端倒计时只是给学生的提示因此不需要太精确但要注意一个常见问题用setInterval每隔1秒减1会在切后台时被微信冻结回到页面时倒计时还是原来的数字学生看到的时间就和真实时间脱节了。// pages/student/timeSign.js const app getApp() Page({ data: { taskId: null, endTime: null, countdown: 0, timer: null }, onLoad(options) { this.setData({ taskId: options.taskId }) this.loadTaskDetail() }, onUnload() { if (this.data.timer) clearInterval(this.data.timer) }, loadTaskDetail() { wx.request({ url: ${app.globalData.baseUrl}/api/task/detail, data: { taskId: this.data.taskId }, success: (res) { if (res.data.code 0) { const endTime new Date(res.data.data.endTime).getTime() this.setData({ endTime }) this.startCountdown() } } }) }, startCountdown() { this.setData({ timer: setInterval(() { const remain Math.floor((this.data.endTime - Date.now()) / 1000) this.setData({ countdown: remain 0 ? remain : 0 }) if (remain 0) { clearInterval(this.data.timer) } }, 500) }) } })倒计时刷新间隔改成500毫秒而不是1秒是为了让“剩最后1秒”的展示不至于停在0那么尴尬。如果用1秒的间隔当后端时间已经到截止时刻前端倒计时可能还显示00:01学生在这个瞬间提交仍会收到“签到已截止”。这里不要用本地Date.now()判断学生把手机时间调快或调慢都会骗过自己但骗不过后台后端的判定以服务器时间为准。4.3 密码签到和手势签到的输入交互密码签到页面UI很简单一个显示剩余时间的文本、一个密码输入框、一个提交按钮。关键点在于提交时要把taskId、密码、学生ID一起打包。// pages/student/passwordSign.js submitPassword() { const password this.data.password.trim() if (!password) { wx.showToast({ title: 请输入签到密码, icon: none }) return } wx.request({ url: ${app.globalData.baseUrl}/api/sign/doSign, method: POST, header: { Content-Type: application/json }, data: { taskId: this.data.taskId, password: password }, success: (res) { if (res.data.code 0) { wx.showToast({ title: 签到成功, icon: success }) } else { wx.showToast({ title: res.data.msg, icon: none }) } } }) }密码校验的提示语不要透露“密码错误”之外的任何信息。后台只返回成功或者失败前端不要自己猜测密码是否正确。还有一个细节input组件的password属性要设成true但真机上输入时不要用confirm-typedone直接提交防止误触。手势签到比密码签到重一些需要在canvas上画九宫格并记录轨迹。原生小程序的canvas触摸事件足够完成这个功能不需要引入组件库。简化版的核心绘制逻辑// utils/gesture.js // 九宫格三个圆心的坐标按1-9编号 const points [ { x: 60, y: 60 }, // 1 { x: 150, y: 60 }, // 2 { x: 240, y: 60 }, // 3 { x: 60, y: 150 }, // 4 { x: 150, y: 150 }, // 5 { x: 240, y: 150 }, // 6 { x: 60, y: 240 }, // 7 { x: 150, y: 240 }, // 8 { x: 240, y: 240 } // 9 ] let selected [] function getSelectedPoint(touchX, touchY) { for (let i 0; i points.length; i) { const dx touchX - points[i].x const dy touchY - points[i].y if (Math.sqrt(dx * dx dy * dy) 40) { return i 1 } } return 0 }触摸移动时每经过一个圆就把编号追加进selected数组要防止同一个点被连续重复记录常见写法是记录上一次的编号当前编号不同才push。手势绘制完成后把selected数组拼接成“1-3-5-7”这样的字符串交给后台校验。很多翻车案例出在学生把“1-2-3”和“1-2-3-2”混在一起后台比对自然失败。因此前端在完成手势后必须显示回放路径让学生确认是完整的路径再提交。手势签到还要区分“教师端录入手势”和“学生端验证手势”两个页面复用同一个gesture组件但暴露不同方法一个返回gestureHash用于保存一个只是把输入序列交付后台。如果不区分很容易在发布任务时把校验逻辑也带到教师端流程里造成发布的签到永远校验不过。4.4 位置签到的wx.getLocation与距离判断位置签到是小程序端比较依赖权限的一类。调用wx.getLocation前需要在app.json里声明字段否则真机上会直接进入fail回调开发者工具里却一切正常。{ permission: { scope.userLocation: { desc: 用于课堂签到定位 } }, requiredPrivateInfos: [ getLocation ] }requiredPrivateInfos是从基础库2.21.3开始需要补充的没有它真机授权后仍然拿不到坐标。这个字段在开发者工具上不强制所以很多人直到扫码真机预览才发现定位失败。wx.getLocation的调用与提交// pages/student/locationSign.js submitLocation() { wx.getLocation({ type: gcj02, isHighAccuracy: true, highAccuracyExpireTime: 3000, success: (res) { wx.request({ url: ${app.globalData.baseUrl}/api/sign/doSign, method: POST, header: { Content-Type: application/json }, data: { taskId: this.data.taskId, latitude: res.latitude, longitude: res.longitude }, success: (resp) { wx.showToast({ title: resp.data.code 0 ? 签到成功 : resp.data.msg, icon: resp.data.code 0 ? success : none }) } }) }, fail: () { wx.showToast({ title: 定位失败请检查定位开关, icon: none }) } }) }type选gcj02是因为它和微信地图坐标系一致后台存储时直接用不要转wgs84。isHighAccuracy在部分Android机型上会明显变慢3秒的过期时间不是等定位结果3秒而是允许系统在这段时间内持续优化精度。位置签到页面建议把“允许距离”展示出来后台返回的allow_distance在发布任务时已经定好前端把当前定位和签到点坐标画在一个缩放图上学生一眼就知道自己为什么签不上。5. 课堂签到项目避坑常见问题与排查指南5.1 签到时间总是不对服务器时区与前端计时器的坑现象上午10点发布的签到学生9点50打开页面就看到“签到还没开始”到10点05分提交却被提示“签到已截止”前后误差正好约8小时。原因MySQL驱动连接串里少了serverTimezoneAsia/Shanghai驱动读取DATETIME列时使用了UTC而Java进程本地时间是东八区两者相减正好偏差8小时。如果项目部署在Linux服务器上服务器系统时区还可能被设置成UTC问题会更隐蔽。解决第一步jdbc.properties连接串补上serverTimezoneAsia/Shanghai第二步检查Tomcat启动脚本里的JAVA_OPTS是否设置-Duser.timezoneAsia/Shanghai第三步前端倒计时不要用本地Date.now()计算剩余时间直接拿接口返回的endTime和服务器当前时间做差值。做到这三步时间窗口的判定就以Java服务端new Date()为唯一基准学生改手机时间也无法提前签到。可以在发布任务接口里顺手返回serverTime字段前端用它校准倒计时。5.2 位置签到在室内总是失败距离阈值与坐标系现象教师在讲台上发布位置签到坐在教室窗户边的学生却总提示“不在签到范围内”明明离讲台直线距离不到20米。原因第一教室内的GPS信号被建筑遮挡手机返回的坐标可能偏移到隔壁楼第二后台距离计算使用的坐标一个来自微信gcj02另一个可能来自高德或者手动填写的百度坐标两个坐标系混用导致误差被放大第三allow_distance设成50米在室内场景下太严格。解决把签到距离阈值至少放到100到200米教室场景按100米起步阶梯教室或者操场可以放宽到300米。坐标统一使用wx.getLocation的gcj02返回后台不要做任何坐标系转换。如果仍然有学生投诉定位飘到几百米外可以在签到记录表里加上网络类型字段排查时看是不是某个运营商的基站定位太差。距离阈值调大后会带来代签风险学生人在宿舍也能签到教师放在教室的任务。如果想兼顾误差和防代签常见做法是限制签到时间段再配合课表里当节课的教室名称展示提示而不是把阈值调到500米以上。5.3 后台返回的JSON前端解析不了日期格式和字段命名现象小程序请求任务详情接口显示正常但列表页的时间字段显示成一大串数字“1698290380000”或者直接白屏。原因后台实体类的Date字段默认被Spring MVC序列化成时间戳小程序端new Date(时间戳)可以解析但如果你用wx.setData直接把时间戳塞进界面渲染出来的就是一串数字。更隐蔽的是后台返回的JSON里如果字段名用了end_time这种带下划线的命名而前端JS用的是endTime也会出现字段undefined页面渲染不报错但数据是空的。解决给Spring MVC配置一个全局的Jackson序列化器把Date统一格式化成“yyyy-MM-dd HH:mm:ss”。在spring-mvc.xml里加一段mvc:annotation-driven mvc:message-converters bean classorg.springframework.http.converter.json.MappingJackson2HttpMessageConverter property nameobjectMapper bean classcom.fasterxml.jackson.databind.ObjectMapper property namedateFormat bean classjava.text.SimpleDateFormat constructor-arg valueyyyy-MM-dd HH:mm:ss/ /bean /property /bean /property /bean /mvc:message-converters /mvc:annotation-driven如果前端字段问题出在MyBatis映射检查实体类字段名和数据库列名是否一致不一致就在resultMap里配columnend_time propertyendTime。跨域问题通常出现在用浏览器调试后台接口时小程序端wx.request不受浏览器同源策略限制但用Postman或者网页调试会碰到。在spring-mvc.xml里配置CorsFilter允许所有来源的GET和POST即可。5.4 手势密码改了却校验不过画布坐标和序列提交时机现象教师在发布页画了手势“1-2-3-5-8”学生也在签到页照着画后台却一直返回手势不正确。原因两个最容易忽略的地方。第一教师在绘制手势时如果手指在圆之间拖动的轨迹经过了某个圆但没停住selected数组里被无意识多加了一个点导致发出时的序列和预期不一致。第二触屏事件里onTouchMove触发频率很高同一时刻多个move事件都把当前点push进了数组一个点被记录多次提交时点序被拉长。解决在记录点序列时做两个过滤。一是当前编号和数组最后一个编号相同时不追加二是每个编号在数组里最多出现一次重复经过时只保留第一次。更好的做法是在onTouchEnd时对selected做一次去重再生成哈希提交。教师端应该把手势序列以“1-2-3-5-8”明文展示在调试面板里正式发布前自己重新走一遍学生端流程不要只画一次就发布。后台接收到手势序列字符串后只比对哈希这样即使拿到数据库也无法逆向还原出手势图案这个设计在项目说明里应当写明“手势明文不落库”。5.5 后台启动或签到写入报错MySQL连接的三类典型报错现象后台Tomcat能启动但第一次请求签到任务列表时报500控制台出现“Access denied for user”或“Public Key Retrieval is not allowed”。原因这三个报错经常混在一起。Access denied是账号密码或者权限不对Public Key Retrieval is not allowed是MySQL 8驱动在非SSL连接下默认不自动拿服务器公钥SSL握手失败是useSSLtrue但证书没配。解决逐条对照。确认jdbc.properties里username和password能和Navicat连接参数完全一致连接串加上useSSLfalse再加上allowPublicKeyRetrievaltrue。如果还报Communications link failure去看服务器防火墙是不是把3306端口封了。本地跑就把host改127.0.0.1不要写localhost避免走socket协议在某些Linux环境下的权限问题。这组坑在后台上线部署时出现频率最高一个项目里通常要同时解决两个以上。6. 从能跑到用得住签到项目的验证思路与可扩展技巧6.1 联调验证不要只跑一遍“签到成功”拿到源码或自己写完代码后第一件事不是立刻部署而是做一份10分钟内的联调检查单。按教师、学生两个视角各测一遍场景操作预期结果教师发布限时签到当前时间加3分钟窗口学生端能看到倒计时学生在开始前提交点签到按钮提示“签到还没开始”学生正常提交窗口内提交提示“签到成功”学生截止后提交结束时间之后提交提示“签到已截止”学生重复提交连续点两次第二次提示“重复签到”这张表可以写进项目说明作为功能验证部分。答辩时最容易被追问的不是签到成功怎么实现而是重复提交怎么防、迟到怎么判定、定位误差怎么处理。把这张表和背后的联合唯一键、服务器时间判定一起写进说明回答的时候可以直接指着代码讲比空口解释可靠得多。6.2 扩展到课表维度签到统计不只在任务层签到项目做到能跑之后最有性价比的扩展是加一张course表把sign_task挂到课表下。现状是sign_task只有teacher_id发布时每次手动填课程名教师首页看不到出勤率。加一个course_id字段后签到记录可以按课程聚合“应到人数、已到人数”这个扩展完全不动四种签到方式的核心代码改发布接口加一个参数列表查询做一个join即可。对于课程设计来说这个扩展直接拉开和省事版的差距。6.3 上线前必调的三个参数最后提醒三个已经踩过无数遍的参数。第一个是位置签到的allow_distance教室场景用100到200米不要用房间精度去要求GPS也不要为了照顾偏远的同学放宽到500米。第二个是限时签到的时间窗口演示建议设3到5分钟太短点提交来不及太长没有“限时”效果。第三个是jdbc.properties里的serverTimezone忘记它带来的时差问题会浪费好几个小时排查。我如今接手这类项目的第一习惯是发布一个距自己只有5米的测试签到用真机跑一遍把四种方式各“作废”一次再确认正常路径。写项目说明时没了当初的血泪经验反而想在allow_distance后面补一句“室内GPS会漂移请勿小于100米”免得下一个接手的人把50米当真。这个方案不复杂复杂的是每个参数背后都有可复现的失败希望帮到你。本文还有配套的精品资源点击获取
