ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

微信小程序+Java+MySQL毕业设计:学生签到系统完整实现与避坑指南

微信小程序+Java+MySQL毕业设计:学生签到系统完整实现与避坑指南 简介这份资源是面向计算机相关专业毕业设计与课程设计场景的微信小程序学生签到系统完整源码包采用Java后端搭配微信小程序前端与MySQL数据库适合需要完成毕设或课设的学生、自学者参考与二次开发。系统划分管理员、教师、学生三类角色管理员负责学生、教师、签到、班课、请假、审批、销假及系统管理教师可发布与管理签到、班课审批请假并查看学生签到与销假情况学生可查看签到任务、加入班课、提交请假并完成销假操作。压缩包共1088个文件约18.8MB包含120个java后端源码、137个vue与137个js前端逻辑、44个wxss与43个wxml小程序页面、2个sql数据库脚本以及png、svg、jpg等界面素材和json、xml、properties等配置文档前后端源码与数据库文件齐全。项目基于JDK1.8、MySQL5.7、Maven3.3与Tomcat7可用eclipse或idea导入小程序端借助hbuildx或微信开发者工具运行。目前已有93人学习可作为完整赛题方案与排错参考。1. 学生签到系统为什么成了毕业设计里的“硬通货”每年到了选题季微信小程序、Java、MySQL 这三个词凑在一起几乎就是计算机毕业设计里最稳的组合。学生签到系统看着简单真动手才发现它把「小程序端交互 Java 后端接口 MySQL 数据落地」这条完整链路全串了一遍答辩时老师随便挑一个点都能问出东西。它解决的核心问题很具体老师发起一次签到学生在手机上点一下后端记录时间、地点、状态最后能导出考勤表。适合谁做适合想拿一个能跑、能演示、能讲清楚技术选型理由的本科或专科毕业设计也适合刚学完 Java 基础、想找个真实项目练手的同学。这一篇不讲虚的从建表到接口到小程序页面把能复现的路径铺出来。2. 技术选型与数据库设计先把地基打对2.1 为什么是 Spring Boot 原生小程序而不是别的很多同学一上来就想用 uniapp 或者各种低代码平台觉得快。但毕业设计答辩时老师更看重你对请求链路的理解。原生微信小程序 Spring Boot 的组合前后端边界清晰出了问题你能定位到是 wx.request 没发出去还是 Controller 没接住。Java 侧用 Spring Boot 而不是纯 Servlet是因为自动装配能省掉大量 XML 配置让你把精力放在业务逻辑上。MySQL 选 5.7 还是 8.0 都行但要注意 8.0 默认的 caching_sha2_password 认证方式老版本驱动连不上会报Unable to load authentication plugin这个坑后面会细说。数据库设计是签到系统的命门。核心就三张表用户表、签到任务表、签到记录表。用户表存学生和老师的身份区分签到任务表存老师发起的每一次签到签到记录表存谁在什么时候签了哪次到。这里有个关键决策签到状态是存在记录表里还是用记录表的有无来表示我一般会在记录表里加一个 status 字段0 表示正常签到1 表示迟到2 表示缺勤由定时任务补录。这样查询统计时不用做复杂的关联判断。2.2 建表 SQL 与字段类型选择-- 用户表区分老师和学生 CREATE TABLE sys_user ( id INT NOT NULL AUTO_INCREMENT COMMENT 主键, 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 COMMENT 真实姓名, role TINYINT NOT NULL DEFAULT 0 COMMENT 0学生 1老师, class_id INT DEFAULT NULL COMMENT 班级ID学生归属, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 签到任务表老师发起的签到 CREATE TABLE sign_task ( id INT NOT NULL AUTO_INCREMENT, teacher_id INT NOT NULL COMMENT 发起老师ID, title VARCHAR(64) NOT NULL COMMENT 签到标题, start_time DATETIME NOT NULL COMMENT 开始时间, end_time DATETIME NOT NULL COMMENT 截止时间, longitude DECIMAL(10,7) DEFAULT NULL COMMENT 签到点经度, latitude DECIMAL(10,7) DEFAULT NULL COMMENT 签到点纬度, radius INT DEFAULT 200 COMMENT 允许签到半径单位米, status TINYINT DEFAULT 1 COMMENT 1进行中 0已结束, PRIMARY KEY (id), KEY idx_teacher (teacher_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT签到任务表; -- 签到记录表学生签到明细 CREATE TABLE sign_record ( id INT NOT NULL AUTO_INCREMENT, task_id INT NOT NULL, student_id INT NOT NULL, sign_time DATETIME DEFAULT NULL COMMENT 实际签到时间, status TINYINT DEFAULT 0 COMMENT 0正常 1迟到 2缺勤, distance INT DEFAULT NULL COMMENT 签到距离单位米, PRIMARY KEY (id), UNIQUE KEY uk_task_student (task_id,student_id), KEY idx_student (student_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT签到记录表;这三张表的字段类型不是随便定的。openid用 VARCHAR(64) 是因为微信返回的 openid 长度不固定但不会超过 64。经纬度用 DECIMAL(10,7) 而不是 FLOAT是因为浮点数在计算距离时会有精度丢失DECIMAL 能保证小数点后 7 位的精度对应到实际距离误差在厘米级。uk_task_student这个唯一索引是关键它从数据库层面防止同一个学生对同一次签到重复插入记录比在代码里查一遍再插要可靠得多。提示建表时字符集统一用 utf8mb4不要用 utf8。MySQL 的 utf8 是残缺的存不了 emoji学生昵称里带个表情符号就会插入失败。2.3 距离计算与签到范围判定签到范围判定是这类系统的技术亮点。常见做法是后端拿到学生的经纬度和签到任务里的经纬度算球面距离超过 radius 就拒绝。公式用 Haversine 就行不用上什么高深算法。// 计算两个经纬度之间的球面距离单位米 public static double getDistance(double lat1, double lng1, double lat2, double lng2) { double radLat1 Math.toRadians(lat1); double radLat2 Math.toRadians(lat2); double a radLat1 - radLat2; double b Math.toRadians(lng1) - Math.toRadians(lng2); // Haversine公式核心 double s 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2))); s s * 6378137.0; // 地球赤道半径单位米 return Math.round(s * 10000) / 10000.0; }这段代码里6378137.0是 WGS84 椭球的长半轴用这个值算出来的距离在校园范围内误差可以忽略。Math.round(s * 10000) / 10000.0是保留四位小数实际存库时我会转成 int 存整米数省空间也够用。参数方面radius 默认给 200 米因为 GPS 在建筑物附近漂移几十米是常事给太小会导致学生明明在教室却签不上给太大又失去了定位签到的意义。3. 后端接口实现从登录到签到的完整链路3.1 微信登录换取 openid 的接口写法小程序端调用wx.login()拿到 code传给后端后端拿 code 去换 openid。这里有个血泪经验appid和secret千万别硬编码在代码里提交到仓库用配置文件或者环境变量。换 openid 的接口是微信官方的用 RestTemplate 或 HttpClient 都行。PostMapping(/wxLogin) public Result wxLogin(RequestBody LoginDTO dto) { // 1. 用code换取openid String url https://api.weixin.qq.com/sns/jscode2session?appid appId secret appSecret js_code dto.getCode() grant_typeauthorization_code; ResponseEntityString resp restTemplate.getForEntity(url, String.class); JSONObject json JSON.parseObject(resp.getBody()); String openid json.getString(openid); if (openid null) { return Result.error(微信登录失败 json.getString(errmsg)); } // 2. 根据openid查用户没有则提示先绑定学号 SysUser user userService.getByOpenid(openid); if (user null) { return Result.error(请先绑定学号); } // 3. 生成token返回这里用简单UUID生产环境建议JWT String token UUID.randomUUID().toString(); redisTemplate.opsForValue().set(token: token, user.getId(), 2, TimeUnit.HOURS); return Result.success(token); }逻辑说明第一步换 openid 是标准流程grant_type固定写authorization_code。第二步查用户如果没绑定就返回错误码让前端跳绑定页。第三步生成 token 存 Redis设置 2 小时过期比 JWT 简单适合毕业设计场景。参数上注意appId和appSecret从配置文件读restTemplate要配好超时时间默认没超时会导致请求卡死。3.2 发起签到与签到提交的接口老师发起签到前端传标题、起止时间、经纬度、半径。后端插入 sign_task 表同时给该班级所有学生生成 sign_record 记录status 默认 2缺勤。这样做的目的是签到结束时不用再遍历学生补缺勤直接查 status2 的就是没签到的。PostMapping(/createTask) public Result createTask(RequestBody TaskDTO dto, RequestHeader(token) String token) { Integer teacherId getUserIdByToken(token); // 1. 插入签到任务 SignTask task new SignTask(); task.setTeacherId(teacherId); task.setTitle(dto.getTitle()); task.setStartTime(dto.getStartTime()); task.setEndTime(dto.getEndTime()); task.setLongitude(dto.getLongitude()); task.setLatitude(dto.getLatitude()); task.setRadius(dto.getRadius() null ? 200 : dto.getRadius()); taskMapper.insert(task); // 2. 给该老师所带班级学生批量生成缺勤记录 ListInteger studentIds userService.getStudentIdsByTeacher(teacherId); for (Integer sid : studentIds) { SignRecord record new SignRecord(); record.setTaskId(task.getId()); record.setStudentId(sid); record.setStatus(2); // 默认缺勤 recordMapper.insert(record); } return Result.success(task.getId()); }学生签到接口要做三件事校验任务是否在有效期内、校验距离、更新记录状态。这里用update ... where task_id? and student_id? and status2来保证只有缺勤状态才能被改成正常或迟到防止重复签到。PostMapping(/doSign) public Result doSign(RequestBody SignDTO dto, RequestHeader(token) String token) { Integer studentId getUserIdByToken(token); SignTask task taskMapper.selectById(dto.getTaskId()); // 1. 时间校验 Date now new Date(); if (now.before(task.getStartTime()) || now.after(task.getEndTime())) { return Result.error(不在签到时间内); } // 2. 距离校验 double distance GeoUtil.getDistance(dto.getLatitude(), dto.getLongitude(), task.getLatitude().doubleValue(), task.getLongitude().doubleValue()); if (distance task.getRadius()) { return Result.error(距离签到点 (int) distance 米超出范围); } // 3. 更新记录只有缺勤状态才能更新 int late now.after(task.getStartTime()) now.before(task.getEndTime()) ? 0 : 1; int rows recordMapper.updateStatus(task.getId(), studentId, late, (int) distance, now); if (rows 0) { return Result.error(请勿重复签到); } return Result.success(签到成功); }参数说明late的判断逻辑是只要在起止时间内都算正常实际可以改成开始后 10 分钟算迟到。rows 0说明该学生记录不是缺勤状态已经被更新过了直接返回重复签到提示。这个设计比先查再更新少一次数据库交互也避免了并发问题。3.3 签到统计与导出接口老师需要看谁签了谁没签还要能导出 Excel。统计接口按 task_id 分组查 sign_record关联 sys_user 拿姓名学号。导出用 EasyExcel 或者 POI 都行EasyExcel 代码量少。GetMapping(/export) public void export(RequestParam Integer taskId, HttpServletResponse response) throws IOException { ListSignRecordVO list recordMapper.selectByTaskId(taskId); response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment;filenamesign_ taskId .xlsx); EasyExcel.write(response.getOutputStream(), SignRecordVO.class) .sheet(签到明细) .doWrite(list); }SignRecordVO里用ExcelProperty注解标好列名EasyExcel 会自动处理表头。注意 response 的 content-type 要设对否则浏览器可能直接打开乱码而不是下载。文件名用 taskId 拼接避免中文乱码问题。4. 小程序端页面与交互把接口串起来4.1 页面结构与顶部导航栏适配小程序端至少需要四个页面登录绑定页、签到列表页、签到详情页、个人记录页。顶部导航栏高度是个老生常谈的坑微信小程序默认导航栏高度在不同机型上不一样用wx.getSystemInfoSync()拿statusBarHeight和navigationBarHeight来算。// 在页面onLoad里获取导航栏高度 onLoad() { const sysInfo wx.getSystemInfoSync(); // 状态栏高度 导航栏高度安卓和iOS不同 const navHeight sysInfo.statusBarHeight 44; this.setData({ navHeight }); }44是 iOS 导航栏的标准高度安卓一般也是 44 或 48用这个值做 padding-top 基本能适配大部分机型。更严谨的做法是用wx.getMenuButtonBoundingClientRect()拿胶囊按钮的位置反推但毕业设计用 44 够了。4.2 签到列表加载更多与下拉刷新签到列表要支持分页加载微信小程序的onReachBottom是天然的分页触发点。配合onPullDownRefresh做刷新。Page({ data: { list: [], page: 1, size: 10, hasMore: true, loading: false }, onLoad() { this.loadList(); }, onPullDownRefresh() { this.setData({ page: 1, list: [], hasMore: true }); this.loadList(() wx.stopPullDownRefresh()); }, onReachBottom() { if (!this.data.hasMore || this.data.loading) return; this.setData({ page: this.data.page 1 }); this.loadList(); }, loadList(callback) { this.setData({ loading: true }); wx.request({ url: ${baseUrl}/task/list?page${this.data.page}size${this.data.size}, header: { token: wx.getStorageSync(token) }, success: (res) { const data res.data.data || []; this.setData({ list: this.data.list.concat(data), hasMore: data.length this.data.size, loading: false }); }, complete: () callback callback() }); } });逻辑说明hasMore的判断依据是本次返回条数是否等于 size等于说明可能还有下一页小于说明到底了。loading标志防止用户快速滑动时重复发请求。onPullDownRefresh里重置 page 和 list 再加载加载完调wx.stopPullDownRefresh()收起刷新动画。参数上 size 给 10 比较合适太小请求频繁太大首屏慢。4.3 签到按钮与定位授权处理签到按钮点击后要先wx.getLocation拿经纬度再调后端接口。这里有个必须处理的场景用户拒绝授权定位怎么办。要在wx.getSetting里检查scope.userLocation没授权就引导去设置页。handleSign() { const that this; wx.getSetting({ success(res) { if (!res.authSetting[scope.userLocation]) { wx.authorize({ scope: scope.userLocation, success() { that.doSign(); }, fail() { wx.showModal({ title: 需要定位权限, content: 请在设置中开启定位后重试, success(m) { if (m.confirm) wx.openSetting(); } }); } }); } else { that.doSign(); } } }); }, doSign() { wx.getLocation({ type: gcj02, success: (loc) { wx.request({ url: ${baseUrl}/sign/doSign, method: POST, header: { token: wx.getStorageSync(token) }, data: { taskId: this.data.taskId, latitude: loc.latitude, longitude: loc.longitude }, success: (res) { wx.showToast({ title: res.data.msg, icon: res.data.code 200 ? success : none }); } }); } }); }type: gcj02是国测局坐标系微信小程序定位默认返回这个后端存的经纬度也要用同一套坐标系否则距离算出来会偏几百米。这是很多同学翻车的地方前端用 gcj02后端存的是 wgs84算距离直接超范围。5. 避坑与排查那些答辩前夜才发现的坑5.1 现象小程序请求后端报“不在以下 request 合法域名列表中”原因微信开发者工具默认开启域名校验你本地localhost或 IP 地址不在合法域名里。解决开发阶段在开发者工具「详情-本地设置」里勾选「不校验合法域名」但正式发布前必须把后端域名配到微信公众平台的 request 合法域名里且必须是 HTTPS。5.2 现象MySQL 8.0 连接报Public Key Retrieval is not allowed原因MySQL 8.0 默认认证插件是 caching_sha2_passwordJDBC 驱动需要显式允许公钥检索。解决连接 URL 后面加allowPublicKeyRetrievaltrueuseSSLfalse或者把用户认证插件改成 mysql_native_password。生产环境建议用 SSL毕业设计本地跑加参数就行。5.3 现象签到距离算出来总是几百公里原因经纬度顺序传反了。Haversine 公式里是(lat1, lng1, lat2, lng2)但微信wx.getLocation返回的对象里 latitude 和 longitude 是分开的有些同学传参时把 longitude 当 latitude 传了。解决打印一下前后端的经纬度值对比确认。纬度范围是 -90 到 90经度是 -180 到 180一眼就能看出来。5.4 现象同一学生能重复签到原因更新记录时没加 status 条件或者唯一索引没建。解决确保sign_record表有uk_task_student唯一索引更新语句带and status2。如果并发高唯一索引会抛异常捕获后返回“请勿重复签到”即可。5.5 现象导出 Excel 打开是乱码原因response 的 content-type 没设对或者文件名没做 URL 编码。解决content-type 用application/vnd.openxmlformats-officedocument.spreadsheetml.sheet文件名用URLEncoder.encode(fileName, UTF-8)处理。EasyExcel 写流之前先把 response 头设好。6. 让签到系统在答辩时多拿两分的三个技巧第一个技巧是加一个「签到热力图」。老师端不只看列表还能看到每次签到的时间分布。实现很简单按 sign_time 的小时分组统计每个小时的签到人数前端用 echarts-for-weixin 画柱状图。这个功能代码量不大但答辩时演示出来很直观老师会觉得你考虑了数据可视化。第二个技巧是给签到接口加限流。用 Redis 的incr做计数器同一个学生 1 秒内只能请求一次签到接口。代码就几行但能讲出「防止脚本刷签到」的安全意识。具体做法是 key 用sign:limit:{studentId}incr后判断如果大于 1 就拒绝然后expire设 1 秒。第三个技巧是准备一份「异常场景演示脚本」。答辩现场网络可能不稳提前把定位超范围、重复签到、不在签到时间这三个场景的报错截图存好演示时直接展示。我当年就是现场定位漂移导致签不上幸好提前截了图老师反而觉得我考虑周全。这个习惯后来一直跟着我凡是依赖外部环境的演示一定准备降级方案。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进