简介:这是一套面向高校计算机相关专业学生的查寝打卡微信小程序毕业设计源码,采用Java+SSM框架开发服务端,小程序端基于uniapp或原生小程序实现,数据库使用MySQL 5.7,适合作为毕业设计、课程设计或小程序入门练手项目。资源包共342个文件,约34.77MB,包含33个java源文件、37个class编译文件、59个jar依赖包,以及24个js、14个wxml、17个wxss等小程序页面文件,另有28个xml配置、31个png与26个jpg界面素材,并附带sql建表脚本、docx说明文档与ppt答辩材料,结构完整。功能上覆盖学生注册、通知公告、课程表查看、寝室签到打卡(获取位置并上传自拍)、事假病假申请与考勤查询,教师端则提供管理员、课程表、学生信息、通知公告、签到审核、请假与考勤统计等管理模块,前后端逻辑清晰。目前已有71人学习下载,可帮助读者快速理解小程序与SSM整合思路,直接用于二次开发或答辩演示。
1. 查寝打卡小程序:从“人盯人”到“数据管人”的最小闭环
查寝这件事,做过辅导员助理或者宿管委员的人都懂:晚上十点半,一层楼几十个寝室,挨个敲门、点名、签字,遇到不在的还要打电话、记名字、第二天上报。一周五天,重复劳动,数据还散落在几张纸质表上。查寝打卡微信小程序源码要解决的,就是把这条链路搬到线上——学生端定位打卡、拍照留痕,管理端实时看统计、导出缺勤名单,后端用 MySQL 存记录,前端用微信小程序原生框架。它适合谁?适合正在做毕业设计、需要一套完整前后端可运行项目的同学,也适合想拿一个真实业务场景练手小程序全栈开发的人。整套东西的核心不是技术多高深,而是把“谁在、谁不在、什么时候查的”这三件事用数据库和接口串起来,形成一个能跑通、能演示、能写进论文的最小闭环。
2. 前后端怎么分工:小程序端、服务端、数据库三层拆解
2.1 为什么选微信小程序原生 + Node.js + MySQL
查寝打卡这个场景有几个硬约束:第一,用户都在微信里,不可能让人装 App;第二,打卡要拿定位和拍照,小程序 API 直接支持;第三,数据量不大,一个学院几千条记录,MySQL 单表足够。所以技术选型上,前端用微信小程序原生框架(WXML + WXSS + JS),不引入 uni-app 或 Taro,原因是毕业设计答辩时原生代码更直观,老师能看懂页面结构和生命周期。后端我一般会选 Node.js + Express,因为 JavaScript 前后端统一,调试成本低,而且 Express 写 RESTful 接口非常快。数据库用 MySQL 5.7 或 8.0 都行,字符集用 utf8mb4,避免学生姓名里的生僻字乱码。
这里有个选型理由要讲清楚:为什么不用云开发?云开发确实省事,但毕业设计通常要求“完整前后端 + MySQL”,云开发的数据库是 JSON 文档型,跟 MySQL 关系型数据库的教学要求不匹配。用自建服务端 + MySQL,虽然多写一些代码,但论文里能画出 ER 图、能写 SQL 语句、能讲事务和索引,工作量和技术深度都更够。
2.2 数据库表设计:四张核心表撑起整个系统
查寝打卡的数据模型不复杂,但要把“学生-寝室-打卡记录-管理员”这四个实体的关系理清楚。下面是我常用的建表 SQL,直接可以在 MySQL 里执行:
-- 学生表:存基本信息,openid 是微信登录的唯一标识 CREATE TABLE `student` ( `id` INT NOT NULL AUTO_INCREMENT, `openid` VARCHAR(64) NOT NULL COMMENT '微信openid', `name` VARCHAR(32) NOT NULL COMMENT '姓名', `student_no` VARCHAR(20) NOT NULL COMMENT '学号', `dorm_building` VARCHAR(20) DEFAULT NULL COMMENT '宿舍楼', `dorm_room` VARCHAR(10) DEFAULT NULL COMMENT '寝室号', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`openid`), KEY `idx_dorm` (`dorm_building`, `dorm_room`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 打卡记录表:每次查寝一条记录,包含定位和照片 CREATE TABLE `checkin_record` ( `id` INT NOT NULL AUTO_INCREMENT, `student_id` INT NOT NULL, `checkin_date` DATE NOT NULL COMMENT '查寝日期', `checkin_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `latitude` DECIMAL(10,7) DEFAULT NULL COMMENT '纬度', `longitude` DECIMAL(10,7) DEFAULT NULL COMMENT '经度', `photo_url` VARCHAR(255) DEFAULT NULL COMMENT '现场照片路径', `status` TINYINT DEFAULT 1 COMMENT '1正常 2迟到 3缺勤', `remark` VARCHAR(255) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_student_date` (`student_id`, `checkin_date`), KEY `idx_date` (`checkin_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 管理员表:宿管或辅导员,用账号密码登录后台 CREATE TABLE `admin` ( `id` INT NOT NULL AUTO_INCREMENT, `username` VARCHAR(32) NOT NULL, `password` VARCHAR(64) NOT NULL COMMENT '存md5或bcrypt哈希', `real_name` VARCHAR(32) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 查寝任务表:管理员发起一次查寝,设定时间范围 CREATE TABLE `checkin_task` ( `id` INT NOT NULL AUTO_INCREMENT, `task_date` DATE NOT NULL, `start_time` DATETIME NOT NULL, `end_time` DATETIME NOT NULL, `created_by` INT NOT NULL, `status` TINYINT DEFAULT 1 COMMENT '1进行中 2已结束', PRIMARY KEY (`id`), KEY `idx_task_date` (`task_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这四张表里,checkin_record的uk_student_date唯一索引是关键——它保证一个学生一天只能打卡一次,防止重复提交。idx_dorm索引用于按寝室批量查询,管理员看“某栋楼哪些人没打卡”时走这个索引。photo_url只存路径,不存二进制,图片文件放服务器本地目录或对象存储,数据库只记路径,这是常规做法,避免数据库膨胀。
2.3 服务端接口:三个必须实现的 RESTful 路由
后端接口不用多,但下面三个是核心,缺一个流程就跑不通。用 Express 写,代码结构按routes/分文件:
// routes/checkin.js const express = require('express'); const router = express.Router(); const db = require('../db'); // mysql2 连接池 // 1. 学生提交打卡:接收 student_id、经纬度、照片路径 router.post('/submit', async (req, res) => { const { studentId, latitude, longitude, photoUrl } = req.body; const today = new Date().toISOString().slice(0, 10); try { // 先查今天是否已打卡,唯一索引兜底 const [exist] = await db.query( 'SELECT id FROM checkin_record WHERE student_id=? AND checkin_date=?', [studentId, today] ); if (exist.length > 0) { return res.json({ code: 400, msg: '今日已打卡' }); } await db.query( `INSERT INTO checkin_record (student_id, checkin_date, latitude, longitude, photo_url, status) VALUES (?, ?, ?, ?, ?, 1)`, [studentId, today, latitude, longitude, photoUrl] ); res.json({ code: 200, msg: '打卡成功' }); } catch (err) { // 唯一索引冲突时也返回已打卡,避免并发重复插入 if (err.code === 'ER_DUP_ENTRY') { return res.json({ code: 400, msg: '今日已打卡' }); } res.status(500).json({ code: 500, msg: '服务端错误' }); } }); // 2. 管理员查询某天缺勤名单 router.get('/absent', async (req, res) => { const { date, building } = req.query; // 左连接:学生表左连打卡记录,记录为 null 的就是没打卡的 const sql = ` SELECT s.id, s.name, s.student_no, s.dorm_building, s.dorm_room FROM student s LEFT JOIN checkin_record r ON s.id = r.student_id AND r.checkin_date = ? WHERE r.id IS NULL ${building ? 'AND s.dorm_building = ?' : ''} `; const params = building ? [date, building] : [date]; const [rows] = await db.query(sql, params); res.json({ code: 200, data: rows }); }); // 3. 统计某天打卡率 router.get('/stats', async (req, res) => { const { date } = req.query; const [[total]] = await db.query('SELECT COUNT(*) AS c FROM student'); const [[checked]] = await db.query( 'SELECT COUNT(*) AS c FROM checkin_record WHERE checkin_date=?', [date] ); res.json({ code: 200, data: { total: total.c, checked: checked.c, rate: total.c ? (checked.c / total.c * 100).toFixed(1) + '%' : '0%' } }); }); module.exports = router;这三个接口的逻辑说明:/submit先查后插,同时靠唯一索引兜底并发;/absent用 LEFT JOIN 找差集,这是 SQL 里查“没做某事的人”的标准写法,比用 NOT IN 性能好,因为 NOT IN 在子查询结果集大时会走全表扫描;/stats算打卡率,两个 COUNT 查询,数据量大时可以合并成一条带子查询的 SQL。参数方面,date统一用YYYY-MM-DD格式,building可选,不传就查全部。
2.4 小程序端打卡页:定位 + 拍照 + 提交
小程序端打卡页的核心是调wx.getLocation和wx.chooseImage,然后把数据 POST 给后端。页面 JS 关键代码如下:
// pages/checkin/checkin.js Page({ data: { latitude: null, longitude: null, photoPath: '' }, // 获取定位 getLocation() { wx.getLocation({ type: 'gcj02', // 火星坐标系,微信定位默认 success: (res) => { this.setData({ latitude: res.latitude, longitude: res.longitude }); }, fail: () => { wx.showToast({ title: '定位失败,请检查权限', icon: 'none' }); } }); }, // 拍照或选图 choosePhoto() { wx.chooseImage({ count: 1, sizeType: ['compressed'], // 压缩图,省流量 sourceType: ['camera'], // 只允许拍照,防止相册作弊 success: (res) => { this.setData({ photoPath: res.tempFilePaths[0] }); } }); }, // 提交打卡 submit() { const { latitude, longitude, photoPath } = this.data; if (!latitude || !photoPath) { return wx.showToast({ title: '请先定位并拍照', icon: 'none' }); } // 先上传图片,拿到服务器返回的 url wx.uploadFile({ url: 'https://your-domain.com/api/upload', filePath: photoPath, name: 'file', success: (uploadRes) => { const photoUrl = JSON.parse(uploadRes.data).url; // 再提交打卡记录 wx.request({ url: 'https://your-domain.com/api/checkin/submit', method: 'POST', data: { studentId: getApp().globalData.studentId, latitude, longitude, photoUrl }, success: (res) => { if (res.data.code === 200) { wx.showToast({ title: '打卡成功' }); } else { wx.showToast({ title: res.data.msg, icon: 'none' }); } } }); } }); } });这段代码里,sourceType: ['camera']是防作弊的关键——只允许现场拍照,不允许从相册选旧图。sizeType: ['compressed']压缩图片,避免上传大图超时。上传和提交分两步,先传图拿 URL,再提交记录,这样数据库里只存路径。定位用gcj02坐标系,和微信内置地图一致,如果后端要做距离校验(比如判断是否在宿舍楼 100 米范围内),需要把学生打卡坐标和宿舍楼基准坐标都转成同一坐标系再算。
3. 避坑指南:查寝打卡小程序最容易翻车的五个地方
3.1 定位漂移导致“人在寝室却打不上卡”
现象:学生明明在寝室,打卡时定位偏到几百米外,后端如果做了距离校验就直接拒绝。原因:微信wx.getLocation在室内或信号差时精度下降,gcj02坐标本身也有偏移。解决:不要用单次定位做硬校验,改成“定位 + 拍照”双因子,定位只做参考,拍照必须现场拍。如果一定要校验距离,把阈值放宽到 500 米,并且允许学生提交申诉,管理员后台手动通过。
3.2 图片上传路径写死导致换服务器就挂
现象:本地测试正常,部署到服务器后图片 404。原因:代码里图片 URL 写成了http://localhost:3000/uploads/xxx.jpg,换域名后前端拿到的路径打不开。解决:数据库只存相对路径/uploads/xxx.jpg,前端拼接时用配置的baseUrl,或者后端返回完整 URL 时从环境变量读域名。这个坑我见过太多次,血泪经验就是:任何跟域名相关的字符串都不要硬编码。
3.3 并发提交导致重复打卡记录
现象:学生手快点了两次提交,数据库里出现两条同一天记录。原因:前端没做按钮防抖,后端也没加唯一约束。解决:前端提交后立即wx.showLoading并禁用按钮;后端checkin_record表加uk_student_date唯一索引,捕获ER_DUP_ENTRY错误返回友好提示。双保险,缺一不可。
3.4 时间范围校验用服务器时间还是客户端时间
现象:管理员设置查寝时间 22:00-22:30,学生手机时间调快 10 分钟,22:35 还能打卡。原因:校验用了客户端传来的时间。解决:所有时间校验以服务器时间为准,后端在/submit里查checkin_task表,判断NOW()是否在start_time和end_time之间。客户端时间只用于显示,不参与逻辑判断。
3.5 MySQL 连接数不够导致接口间歇性 500
现象:演示时几个人同时打卡,偶尔报 500,过一会又好了。原因:每次请求都mysql.createConnection,没有用连接池,并发一高就超过 MySQL 最大连接数。解决:用mysql2的createPool,配置connectionLimit: 10,所有查询走pool.query。连接池会自动复用连接,避免频繁握手开销。这个坑在毕业设计答辩现场特别致命,因为老师可能同时点好几个页面。
4. 从能跑到好用:三个进阶技巧和验证方法
4.1 用定时任务自动生成缺勤记录
现在缺勤是靠管理员手动查,更省事的做法是每天查寝时间结束后,自动把没打卡的学生写进缺勤表。用 Node.js 的node-cron每天 22:30 跑一次:
const cron = require('node-cron'); // 每天22:30执行,把当天未打卡学生标记为缺勤 cron.schedule('30 22 * * *', async () => { const today = new Date().toISOString().slice(0, 10); await db.query(` INSERT INTO checkin_record (student_id, checkin_date, status, remark) SELECT s.id, ?, 3, '系统自动标记缺勤' FROM student s LEFT JOIN checkin_record r ON s.id = r.student_id AND r.checkin_date = ? WHERE r.id IS NULL `, [today, today]); console.log('缺勤标记完成:', today); });这段 SQL 用INSERT ... SELECT一次性把缺勤学生插进记录表,status=3表示缺勤。注意要放在查寝任务结束之后执行,否则会把正在打卡的人误标。验证方法:手动把服务器时间调到 22:31,看数据库里是否多出缺勤记录。
4.2 打卡率统计加缓存,避免每次全表扫描
学生表几千条时,COUNT(*)很快,但数据量到几万条、每天多次查询时,可以加一层内存缓存。用node-cache把当天的统计结果缓存 5 分钟:
const NodeCache = require('node-cache'); const statsCache = new NodeCache({ stdTTL: 300 }); // 5分钟过期 router.get('/stats', async (req, res) => { const { date } = req.query; const cacheKey = `stats_${date}`; const cached = statsCache.get(cacheKey); if (cached) return res.json({ code: 200, data: cached }); // ... 原查询逻辑 statsCache.set(cacheKey, result); res.json({ code: 200, data: result }); });参数说明:stdTTL: 300表示缓存 5 分钟,查寝期间数据变化快,缓存时间不宜过长;查寝结束后可以手动statsCache.del(cacheKey)清掉。验证方法:连续请求两次/stats,第二次看日志里有没有走数据库查询。
4.3 用 Postman 做接口回归,答辩前跑一遍
毕业设计答辩前,最怕演示时接口挂了。我一般会建一个 Postman Collection,把/submit、/absent、/stats三个接口的请求存好,每次改完代码跑一遍。重点验证三个边界:重复提交返回 400、缺勤查询返回空数组而不是报错、统计接口在零打卡时返回 0% 而不是 NaN。这个习惯帮我省过好几次现场翻车——有一次改完 SQL 忘了测/absent,结果building参数不传时 SQL 拼接多了个AND,直接语法错误。从那以后,任何 SQL 改动我都先跑 Postman 再提交。
希望帮到你。
本文还有配套的精品资源,点击获取