恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
基于Ruoyi和uniapp的学生宿舍考勤系统:前后端分离实践详解
首页
资讯中心
/
基于Ruoyi和uniapp的学生宿舍考勤系统:前后端分离实践详解
基于Ruoyi和uniapp的学生宿舍考勤系统:前后端分离实践详解
发布时间:2026/10/18 18:32:31
简介基于若依Ruoyi框架与uniapp技术实现的前后端分离学生宿舍考勤管理系统面向校园信息化建设人员、Java/前端开发者及需要优化考勤流程的宿舍管理者。系统针对传统人工点名效率低、数据分散等问题提供了口头点名、普通签到、自定义范围位置签到、二维码签到、人脸识别签到、九宫格手势签到、签到码签到等多种方式同时集成阿里云人脸识别与OSS存储支持微信小程序密码和授权登录在无GPS室内环境亦可完成考勤兼顾灵活性与安全性。资源压缩包共82个文件以Java源码、class字节码、XML/YML/Properties配置为主要内容辅以PNG、JPG界面展示图包体仅1.77MB适合快速下载与代码研读。目前已有176人学习浏览。通过完整源码和截图可掌握前后端分离项目结构、考勤数据可视化看板、移动端工作台与请假管理等模块的设计思路对毕业设计、课程设计及企业考勤系统二次开发都很有参考价值。1. 基于Ruoyi和uniapp的学生宿舍考勤一个能直接跑的后台与移动端分离实践如果你手里正好有一份基于Ruoyi框架改造的学生宿舍考勤管理系统源码前端选了uniapp做跨端后端拿着若依脚手架开刀那你大概率想知道这套东西到底能不能落地、考勤的几种方式口头点名、普通签到、位置签到是怎么串起来的。我拆过同类项目负责任地说它解决的不是“能不能记录”的问题而是“考勤场景怎么组织”的问题——谁发起点名、学生怎么参与、位置签到怎么校验、数据回流给谁。它适合正在做毕业设计、课设或者在中小型后台系统里想快速搭一套考勤模块的开发者。后端的若依体系轻车熟路前端用uniapp一套代码覆盖App和小程序整个链路是通的但细节坑不少下面从结构到接口再到踩坑一条条过。2. 整体架构与核心设计从若依脚手架到考勤业务的改造边界2.1 若依作为后端基座哪些能直接用哪些必须改若依RuoYi在国内中小型后台项目里的普及率很高它的价值在于已经给你铺好了四件套基于Spring Boot的后端工程、基于Vue的管理后台前端、基于MySQL的数据库脚本、以及一套完整的权限控制体系菜单、角色、部门。做宿舍考勤系统时我的建议是尽量复用它的“地基”而不是重新造轮子。用户体系若依自带sys_user表包含用户ID、部门ID、手机号、状态等字段。学生可以挂在某个部门下比如按班级或楼栋建部门这样权限隔离直接吃若依的部门数据权限。菜单与按钮权限考勤管理、签到记录查询、统计报表这些页面直接注册成菜单然后分配给对应角色。不需要自己写权限判断前端路由守卫和后端拦截器都现成。代码生成器这是若依最让人省心的功能。针对考勤业务可以先用它生成attendance_record考勤记录和attendance_task考勤任务两张表的CRUD基础代码再往生成的Service实现类里填业务逻辑省掉一大半体力活。必须动手改的点若依默认的用户表没有“学号”“班级”“宿舍楼栋”这些属性。我的做法是在sys_user表上追加扩展字段或者单独建一张student_info表用user_id关联。前者简单直接后者更干净看你的表设计洁癖程度。2.2 uniapp前端如何组织页面与请求层uniapp这边的页面结构按“学生端”来规划核心是三个Tab页首页展示当天考勤状态与待签任务、考勤记录历史列表、我的个人信息与设置。但真正考勤时进去的页面是按签到类型区分的// request.js —— 封装uniapp的网络请求统一带上token const BASE_URL http://你的后端IP:8080 export function request(options) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Authorization: uni.getStorageSync(token), // 若依登录后返回的token存这里 ...options.header }, success: (res) { // 若依统一返回 { code: 200, msg: 操作成功, data: ... } if (res.data.code 200) { resolve(res.data.data) } else if (res.data.code 401) { uni.navigateTo({ url: /pages/login/login }) // token过期回登录页 } else { uni.showToast({ title: res.data.msg, icon: none }) reject(res.data) } }, fail: (err) reject(err) }) }) }这段代码把若依后端AjaxResult的返回结构直接对接上了。有一个容易被新手忽略的细节Authorization头必须带Bearer前缀否则若依的安全过滤器会直接拒绝。另外uni.request的header是静态对象如果每个请求都要动态加token建议把getStorageSync放在每次请求发起时读取而不是模块加载时读一次——因为登录态可能中途变化读一次缓存会有使用上一个登录用户的token的隐患。2.3 定位与地图位置签到绕不开的选型问题位置签到的核心是“拿到学生的经纬度和后端记录的教室或宿舍楼经纬度做距离比对”。uniapp里获取定位有两种方式uni.getLocation直接调起手机GPS或网络定位返回latitude和longitude。在App端需要勾选定位权限在小程序端需要在manifest.json里声明requiredPrivateInfos为[getLocation]。地图SDK如果你还打算显示地图、标出签到点那通常得接入高德或腾讯地图的uni_modules插件。它们的功能更全但也会让首次打包体积增加不少。我的建议是只拿定位数据用uni.getLocation不引入地图SDK。因为考勤页面通常是一个“我到了”按钮点到之后直接把经纬度传给后端比对不需要可视化地图。如果老师想看分布图那在后台管理端用ECharts画散点图反而更直观——这属于后话了。后续在第五章节我会重点讲定位精度和模拟定位的坑。3. 数据库设计与核心表结构考勤任务、记录与位置明细怎么建表3.1 考勤主表与业务状态机考勤任务与考勤记录是两条不同的业务链路我先说任务表。一张attendance_task表记录“某次考勤发布”字段设计如下字段名类型说明idbigint主键task_namevarchar(100)任务名称比如“上午第一节课点名”task_typechar(1)1-口头点名、2-普通签到、3-位置签到class_idbigint关联班级或宿舍楼栋IDcreator_idbigint发起人ID通常是辅导员或宿管start_timedatetime签到开始时间end_timedatetime签到截止时间location_latdecimal(10,7)位置签到的中心纬度可选字段location_lngdecimal(10,7)位置签到的中心经度可选字段radiusint允许偏离半径单位米默认200statuschar(1)0-未开始、1-进行中、2-已结束这张表里最容易漏掉的是status字段。许多初版设计只依赖时间判断当前任务是否有效但时间判断有一个漏洞后端服务器的时间和学生手机时间不一致时会直接判错。所以在接口层我一般会先更新状态根据当前时间和start_time/end_time比较再处理签到逻辑让状态字段成为唯一依据。考勤记录表attendance_record则存储每次签到的结果字段名类型说明idbigint主键task_idbigint关联考勤任务IDstudent_idbigint学生用户IDsign_timedatetime签到时间sign_typechar(1)冗余存储任务类型方便统计查询latdecimal(10,7)学生提交的纬度位置签到才有值lngdecimal(10,7)学生提交的经度statuschar(1)1-正常、2-迟到、3-缺勤、4-位置异常remarkvarchar(500)备注比如口头点名的“答到”或“未答”3.2 位置距离计算不一定要用数据库函数位置签到后端要处理的核心逻辑是“算距离”。网上最常见的方案是Haversine公式它的计算精度在几百米范围内完全够用。我在Service层写了一个工具方法/** * 计算两个经纬度之间的距离单位米 */ 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); 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) )); return s * 6371000; // 地球半径6371km这里直接乘得到米 }这段代码在Java后端的Service实现类里调用比如AttendanceServiceImpl.signByLocation()。调用时从任务表查出中心点和半径再和学生提交的经纬度做比对double distance getDistance( task.getLocationLat(), task.getLocationLng(), studentLat, studentLng ); if (distance task.getRadius()) { record.setStatus(4); // 位置异常 record.setRemark(距离签到点 String.format(%.1f, distance) 米); } else { record.setStatus(1); // 正常 }注意这里用BigDecimal存经纬度而不是double原因是double在MySQL里的精度误差会随表数据量增大被放大。另外因为这类定位比较是应用层逻辑我不推荐用MySQL自带的ST_Distance函数——虽然也能算但后期如果要换成别的数据库比如PostgreSQL改SQL的代价比改Java方法高。3.3 普通签到的“重复提交”防线普通签到的业务比较简单学生点击“签到”后端根据当前任务ID和学生ID查attendance_record如果已有记录则拒绝重复提交。这段判重逻辑虽然简单但不能省原因后面第5章会结合并发场景详细说明。4. 后端接口实现与前端页面串联三种签到方式的请求时序4.1 发起考勤任务管理端的接口与页面联动后台管理端Vue管理后台的“发起考勤”页面本质上是一个表单提交动作。填写任务名称、选择签到方式、设置时间范围和位置签到参数若选位置签到提交后调用POST /attendance/task接口。接口内部逻辑如下PostMapping(/task) public AjaxResult createTask(RequestBody AttendanceTask task) { // 若依里只需要返回AjaxResult.success()前端会自动拦截统一code if (task.getTaskType().equals(3) task.getRadius() null) { task.setRadius(200); // 默认允许200米 } task.setStatus(0); // 新建任务默认未开始 attendanceTaskService.insertTask(task); return AjaxResult.success(考勤任务创建成功); }创建完任务后前端把任务ID传进“开始考勤”按钮调用PUT /attendance/task/{id}/start。这个开始动作在Service层做的事情是更新状态为“进行中”然后向该班级/宿舍楼下的所有学生推送通知。推送可以用uni-push或微信订阅消息但如果是内部测试系统也可以不推让学生主动进首页看到待签任务。4.2 口头点名由管理端操作的后台收卷逻辑口头点名的特殊之处在于发起端是管理端学生端只需要进入页面点击“我在”按钮。它的时序是管理端进入“去点名”页面看到该任务下的学生名单逐个点击学生姓名右侧的“已到”或“未到”按钮每点击一次前端调用PUT /attendance/record/updateStatus接口把该学生的status更新为对应的值。这个接口省掉了学生端的参与但对管理端的操作流畅度有要求。前端在实现时通常会先把整份名单一次加载到浏览器内存里点击时只更新本地的row.status然后用setTimeout在操作停止后统一提交。这个“批量提交”设计能有效减少请求次数。4.3 位置签到完整时序从获取定位到提交结果位置签到是三种方式里链路最长的一个前端核心代码// pages/sign/locationSign.vue 中的核心提交逻辑 async submitLocationSign(taskId) { uni.showLoading({ title: 定位中 }) // 第一步获取当前定位 const location await new Promise((resolve, reject) { uni.getLocation({ type: wgs84, // 默认wgs84坐标系如果后端地图是国内坐标系需要注意转换 success: resolve, fail: () { uni.showToast({ title: 定位失败请检查GPS, icon: none }) reject() } }) }) // 第二步调后端签到接口 try { const res await request({ url: /attendance/task/${taskId}/sign, method: POST, data: { lat: location.latitude, lng: location.longitude } }) uni.showToast({ title: res.msg || 签到成功, icon: success }) } finally { uni.hideLoading() } }这段代码有两个隐患值得一提。一是type: wgs84问题国内地图服务用的大多是gcj02坐标系而uni.getLocation返回的坐标是GPS原生坐标wgs84两者偏移量大约几百米。如果你的签到点在学校宿舍楼内且半径设置只有100米那么wgs84和gcj02的误差会让学生明明站在签到点里却被判“位置异常”。我的做法是后端统一用wgs84计算签到点坐标在录入时也用wgs84坐标——这样两边虽然都偏移但相对距离不偏移。二是uni.getLocation在部分安卓机型上可能一次拿不到结果建议加一个重试机制连续两次失败再提示用户。4.4 普通签到学生端最轻量的一个页面普通签到的前端页面就是一个“一键签到”按钮点击后直接调POST /attendance/task/{id}/sign后端默认跳过位置校验只做时间校验和重复校验。因为整个链路短它是最不容易出问题的场景但也是后面并发测试的重点对象。5. 避坑与常见问题排查时间不一致、定位偏移、并发重复提交5.1 学生手机时间不准导致签到被误判现象明明还在签到截止时间之前后端却提示“签到已结束”或者后端记录的时间与学生手机显示的时间相差很大。原因后端使用服务器系统时间判断前端使用new Date()判断这两者之间没有校准机制。如果学生手机手动设了时间或手机时区设置错误就会出现偏差。解决所有时间判断全部以后端服务器时间为准。前端只负责收集用户动作不参与“是否超时”的判断。具体实现是在创建任务时后端会自动以服务器当前时间为基准计算剩余截止时间前端倒计时只是展示给用户看的参考值。符号上落实后端时间判断不要依赖请求中的sign_time字段一律使用LocalDateTime.now()。5.2 定位偏移几百米坐标系的玄学现象学生站在签到点正中心却提示距离签到点300米。原因正如4.3节提到的wgs84和gcj02坐标系的偏移。更隐蔽的是如果后台管理页面的地图组件是腾讯或高德地图它们默认使用gcj02坐标而学生端uni.getLocation在App端返回的可能又是wgs84。两边一旦混用签名点坐标和实际位置坐标就不在同一个坐标系里。解决在项目里定一条铁律数据库里所有经纬度统一存wgs84。管理端录入签到点坐标后如果通过地图选点会在前端先把gcj02转成wgs84再提交。提供一个转换工具函数// gcj02 转 wgs84简化的公开算法实现能修正大部分偏移 function gcj02ToWgs84(lng, lat) { const a 6378245.0 const ee 0.00669342162296594323 // 这里省略 outOfChina 检查与核心转换公式transformLat/transformLng // 常见实现可搜索 “GCJ-02 to WGS-84 JavaScript” 拿到完整函数 return { longitude: lng, latitude: lat } }把转换函数放在公共工具文件utils/coord.js里所有涉及地图选点的页面都引用它。后端不参与坐标转换只当它是一个黑匣子负责存和算。5.3 并发重复提交高点击率下的重复签到记录现象多个学生同时点击签到或者同一个学生快速点了两次后端生成了两条或三条attendance_record记录。原因大部分人的第一版判重代码是“先查再插”——先查是否存在记录不存在就插入。在并发场景下两个请求同时查都发现不存在然后都插入这时一定会产生重复记录。MySQL默认的REPEATABLE READ隔离级别下这种“查-插”组合并不是原子的。解决给attendance_record表加一个唯一索引让“同一个学生同一个任务只能有一条记录”成为数据库层面的约束ALTER TABLE attendance_record ADD UNIQUE INDEX uk_task_student (task_id, student_id);然后后端捕获DuplicateKeyException把冲突时的表现改成“已经签到过了请勿重复操作”就像这样try { attendanceRecordMapper.insert(record); } catch (DuplicateKeyException e) { return AjaxResult.error(您已经签到过了); }加了唯一索引之后即使前端代码有bug、后端判重逻辑有疏漏数据库也会兜底挡住。这算是给注册和考勤这类“只能一次”的业务上的双保险。5.4 学生端看不到待签任务列表接口的缓存问题现象老师已经在管理端创建了考勤任务并点击“开始”但学生端下拉刷新列表始终不显示。原因如果你在列表查询时对任务状态做了缓存比如用了Redis缓存attendance_task表且没有在状态更新时主动删除缓存那么学生端拿到的永远是旧状态。解决要么根本不加缓存考勤任务量级不大MySQL每次查都吃得消要么在updateStatus方法里显式调用redisTemplate.delete(attendance:task: taskId)。如果用了若依的CacheUtils注意清除时要带上前缀只删单个任务不是清整个列表——否则你删了一个任务列表的重建会让所有学生端同一个时间点一起打库反而更慢。6. 进阶技巧让考勤系统同时支持“哪几个人没签”的高效查询与防代签校验6.1 缺勤名单的构造思路统计缺勤名单时很多人的第一反应是“从考勤记录表里筛status ! 正常的人”。这个思路在“所有人都签了”的情况下反而漏掉了一种人——压根没点进签到页面的人。正确写法是从学生表中取出该班或该楼栋的全部学生再和签到记录做LEFT JOIN签到记录为NULL的就是缺勤。等于用“应到场名单”和“已到场名单”做差集而不是直接查“没签到记录的学生”。让SQL自己写这个差集SELECT s.user_id, s.nick_name FROM sys_user s LEFT JOIN attendance_record ar ON ar.student_id s.user_id AND ar.task_id #{taskId} WHERE s.dept_id #{classId} AND ar.id IS NULL;LEFT JOIN的关键是关联条件必须带上task_id否则会把该学生在历史任务里的签到记录也算进来结果自然错乱。6.2 防代签位置签到之外的“时间窗”校验位置签到能防“没到现场”的但防不了“人到现场帮别人签”的情况。一种低成本做法是给签到加一个时间窗控制同一个考勤任务系统随机生成一个签到口令老师口头告知学生。每个学生每次生成的口令不一样后端校验口令和时间窗后才写入记录。这个技巧适合封闭式课堂或宿舍楼栋考勤。实现时在后端的signByLocation接口里追加两个参数sign_code学生输入的动态码和submit_time。动态码可以用简单的哈希拼接后取前四位或者用存库的随机字符串。// 签到前校验动态口令伪代码 if (!attendanceService.validateSignCode(taskId, userId, signCode)) { return AjaxResult.error(口令错误请确认老师播报的最新口令); } if (Duration.between(serverNow, submitTime).abs().toMinutes() 2) { return AjaxResult.error(提交时间与服务器时间偏差过大); }用这个思路即使学生把位置签到页截图发给另一位同学对方没有实时动态码也签不上。从那以后我在设计任何考勤系统时都会强制走一遍“人员差集 唯一索引 时间窗校验”这套三个动作。它们各自解决一个层面的漏洞合起来能让项目的可信度上一个台阶。希望帮到你。本文还有配套的精品资源点击获取