资讯动态

若依+uniapp打造宿舍考勤系统:前后端分离方案解析

发布时间:2026/10/18 16:24:21 来源:尧图企业网站定制
简介基于Ruoyi框架与uniapp构建的前后端分离学生宿舍考勤管理系统源码包面向高校宿管人员、毕业设计学生及Java全栈开发者。项目覆盖口头点名、普通签到、自定义范围位置签到、二维码、人脸识别、九宫格手势及签到码等多种签到方式并通过阿里云人脸识别与OSS存储保障识别与图片数据存取同时配合移动端工作台、通知公告、请假管理等模块能够直观解决传统考勤效率低、场景受限等问题。压缩包内共82个文件约1.77MB以Java源码24个java和编译产物25个class为主辅以XML、YAML、Properties等配置文件以及PNG、JPG界面截图和Markdown说明文档结构清晰便于二次开发。已有176人学习下载适合需要快速上手或借鉴完整签到流程设计的开发者可直接根据源码梳理前后端交互、阿里云API集成以及微信小程序密码/授权登录的实现思路并基于可视化图表扩展考勤分析功能。1. 学生宿舍考勤管理系统为什么一个签到功能要前后端分离来做宿舍查寝点名这件事做过辅导员或者宿管的人都清楚晚上拿着纸质名单逐间宿舍问“人在不在”有人代签、有人谎报最后统计结果还得手动录进Excel。基于Ruoyiuniapp的前后端分离项目要解决的正是这个问题——用若依搭后台管理、用uniapp做学生端把口头点名签到、普通签到、位置签到三种模式全落到手机里让考勤从“一张纸”变成可追溯的数据流。这个方案重点面向高校宿管中心、学生工作处之类场景也适配中职学校和企业宿舍。学生端是App或微信小程序管理端是Web后台。适合谁适合手上有Spring Boot和若依基础、想在两周内交付一套能演示能上线的考勤系统的团队而不是从零开始设计权限模型和用户体系的人。接下来我按选型、建表、签到判定、前端对接、避坑这条完整链路来讲你不需要见过别人的项目源码跟着思路完全可以自己搭一套。2. 选定骨架Ruoyi管后台、uniapp管学生端各自吃掉哪块2.1 Ruoyi若依做考勤后台白嫖RBAC、代码生成器和定时任务若依在国内Java圈用得非常普遍它的核心竞争力不是那套代码生成器而是RBAC权限模型、数据权限、操作日志、定时任务、部门管理这些已经沉淀好的底座。做宿舍考勤管理系统学生表、宿舍楼栋、考勤记录这些CRUD可以自己写但权限和后台框架一定要复用来否则光做角色权限就要加两周班。你想想宿管、辅导员、学生处、系主任这四种角色看数据的范围都不一样宿舍楼的数据权限按楼栋划分这要是自己从Session里手写拦截器后期每加一个接口都要提心吊胆。我的常见做法是把若依当成半成品后台学生映射到若依的用户表宿舍楼栋映射到部门的树形结构。比如“1号宿舍楼/2层/201室”就是一条部门路径学生隶属于这个部门节点。这样做的好处很明显菜单权限、数据权限都不用重新设计新来的辅导员账号由管理员分配角色即可宿管能看哪些楼的考勤直接交给若依的数据权限字段去卡。曾有个同事试图把宿舍独立成一张楼栋表再手动写过滤最后他发现每次查询都要拼接一堆SQL条件还容易漏权限纯粹是给自己挖坑。部署时我一般用若依自带的代码生成器先把考勤任务表和记录表生成一套标准增删改查页面但签到判定逻辑不放在生成代码里而是独立抽一个AttendanceService。原因是签到有并发和状态判定通用模板改起来反而容易翻车。下面的启动命令是一个最小可运行步骤# 1. 后端进入到若依项目的 admin 模块所在目录 cd ruoyi-admin # 2. 启动后端服务默认端口 8080 mvn spring-boot:run # 3. 前端另开终端进入 ruoyi-ui 目录 cd ../ruoyi-ui # 4. 安装依赖并启动开发服务器默认端口见 vite.config 里的 port npm install npm run dev这段命令的逻辑是先把后端Java服务拉起来再把前端Vue开发服务器跑起来。参数说明如果你在若依多模块的根目录执行可以用mvn -pl ruoyi-admin -am spring-boot:run指定模块并同时编译依赖模块npm run dev启动的是开发模式如果要部署上线应该执行npm run build:prod然后把dist目录交给Nginx托管后端接口通过Nginx的反向代理转发到8080端口。这样配置能避开开发环境的跨域限制也是前后端分离项目最标准的部署姿势。2.2 uniapp做学生端多端发布是甜头弱网与定位差异是代价学生端用uniapp的价值在于一次编写、多端发布。宿舍考勤场景里有的学校要求学生装App有的学校只允许用微信小程序uniapp刚好能一套代码同时输出。但我有一条血泪经验不要指望uniapp的API在安卓、iOS、微信小程序上的行为完全一致尤其是定位授权和网络请求差异极大。前期如果不做条件编译隔离后期接踵而至的兼容问题会改到怀疑人生。位置签到的实现上我建议把定位能力单独封装成一个工具模块内部用条件编译区分平台。小程序端用uni.getLocation没问题但App端一定要先在manifest.json里声明定位权限否则安卓10以上直接返回“用户拒绝授权”。另外在弱网宿舍里签到请求常有超时前端要在fail回调里给用户明确的“重试”提示而不是静默失败。这里可以做一层本地缓存请求失败后把签到动作暂存到Storage等网络恢复后再补交但要注意补交的时间以后端服务器时间为准不能拿本地缓存时间去算。2.3 三端功能边界哪些职责属于若依、哪些属于uniapp、哪些属于数据库很多人做这类系统会犯一个错把签到业务逻辑铺得到处都是。有的写在Vue页面里有的写在SQL存储过程里还有的写在uniapp的onLoad里。我的建议是把职责卡死若依后端只负责鉴权、任务管理、签到判定和统计uniapp只负责采集学生操作和传感器数据数据库只负责持久化和唯一约束。我把这三端的分工做成一张表方便你在设计阶段就对着分职责模块若依后端uniapp端数据库端登录鉴权签发Token、校验密码存储Token并自动携带存储用户与角色关系签到任务创建任务、控制时间窗口展示任务列表、倒计时存储任务与签到记录位置判定计算距离并返回结果采集经纬度并上报记录判定结果防重复签到先查后插、状态校验按钮置灰禁用唯一索引兜底举个边界例子判断“学生是否在宿舍范围内”必须在后端做uniapp把经纬度传上去后端用距离公式算而“设备指纹”比如MAC地址或随机UUID由uniapp在本地生成并存储签到提交时带上。这样即使学生伪造请求也绕不过服务端的距离校验。三端分工清晰后排错也简单签到失败先看是不是前端没传参再看后端日志的租户ID和数据权限最后才怀疑SQL。有一次学生反馈签到一直报“无权限”查了半天是后端接口没加PreAuthorize注解若依的权限拦截把请求默认拒了这种问题在职责混乱的项目里会藏得很深。3. 后端落地考勤表怎么建四种签到判定怎么写3.1 考勤任务表、考勤记录表与宿舍关系的字段设计设计考勤表时我见过很多半路出家的库表把签到按钮状态放在记录表里一次签到能查半天。我的做法是拆成两张核心表任务表和记录表。任务表管一次查寝/点名是什么类型、何时开始结束、位置中心点在哪记录表管每个学生这次签到的结果。为什么要拆因为口头点名、普通签到、位置签到、自动签到四种任务每个任务的参数不一样记录表只需要关心“结果”任务参数变化不影响记录结构。任务表里最关键的是task_type和start_time/end_time。task_type决定前端渲染什么签到交互start_time/end_time决定签到窗口位置签到和自动签到任务还会用到location_lat/location_lng/location_radius/wifi_mac这几个扩展字段。把它们都放在任务表而不是单独建一张扩展表是因为这四种签到模式是固定的没必要引入复杂的EAV结构增加理解成本。-- 考勤任务表记录一次查寝/点名任务 CREATE TABLE dorm_attendance_task ( id BIGINT AUTO_INCREMENT PRIMARY KEY, task_name VARCHAR(64) NOT NULL COMMENT 任务名称, task_type TINYINT NOT NULL COMMENT 1口头点名 2普通签到 3位置签到 4自动签到, dorm_building_id BIGINT NULL COMMENT 关联宿舍楼栋部门ID, start_time DATETIME NOT NULL COMMENT 签到开始时间, end_time DATETIME NOT NULL COMMENT 签到结束时间, late_threshold INT NULL COMMENT 迟到阈值分钟null表示不判断迟到, location_lat DECIMAL(10,7) NULL COMMENT 位置签到中心纬度, location_lng DECIMAL(10,7) NULL COMMENT 位置签到中心经度, location_radius INT NULL COMMENT 允许签到半径米, wifi_ssid VARCHAR(64) NULL COMMENT 自动签到允许的WiFi SSID, status TINYINT NOT NULL DEFAULT 0 COMMENT 0未开始 1进行中 2已结束, create_by VARCHAR(64), create_time DATETIME ); -- 考勤记录表学生每次签到的明细 CREATE TABLE dorm_attendance_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, task_id BIGINT NOT NULL COMMENT 考勤任务ID, student_id BIGINT NOT NULL COMMENT 学生用户ID, student_name VARCHAR(32) NOT NULL COMMENT 冗余姓名方便统计, dorm_full_path VARCHAR(128) NULL COMMENT 宿舍快照如 1号楼/2层/201室, sign_time DATETIME NOT NULL COMMENT 签到时间, sign_type TINYINT NOT NULL COMMENT 实际签到方式1口头、2普通、3位置、4自动、5人工兜底, sign_location VARCHAR(255) NULL COMMENT 签到位置描述或经纬度文本, sign_status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 2迟到 3缺勤 4待复核, device_id VARCHAR(64) NULL COMMENT 设备指纹排查代签用, remark VARCHAR(255), UNIQUE KEY uk_task_student (task_id, student_id), KEY idx_dorm_path (dorm_full_path), KEY idx_sign_time (sign_time) );建好这两张表后有几个字段是我反复踩坑后才加上的sign_location只存文本不做空间索引因为考勤判定不依赖数据库空间计算Java层算好再存结果就行device_id不是安全措施只是排查手段dorm_full_path是冗余的快照字段学生调宿后历史考勤仍能归属到旧宿舍避免外键关联被连带修改。数据量上一张宿舍考勤表一年也就几十万行加普通索引即可没必要上分区。uk_task_student唯一索引是防重复的最后一道防线代码里先查后插能拦住90%的重复并发场景剩下的10%就靠它兜底。3.2 口头点名与普通签到的服务端判定流程口头点名在实现上有两种流派一种是老师逐个点击学生标记已到学生端纯被动另一种是老师发起提问学生端主动点确认。我做的话选第二种因为它能留下一份“学生知情并确认”的操作轨迹避免事后学生说“老师你替我点的”。流程是老师创建口头点名任务Web端广播开始学生端uniapp收到通知后弹出确认卡片学生点确认后端记录一条带签时间、确认人、任务ID的记录若30秒未确认老师可以代为标记未到或者手动强制确认。普通签到的服务端逻辑核心是时间窗口。任务开始前30秒才允许生成签到码学生提交的时候后端先判断当前时间是否在start_time和end_time内再判断学生是否已经签到过。要注意的是时间判断必须只信任服务器时间不能让前端传一个signTime参数进来否则学生抓包改本地时间就能绕过迟到判定。public SignResult normalSign(AttendanceTask task, Long studentId, String deviceId) { Date now new Date(); // 1. 时间窗口校验签到必须落在任务配置的窗口内 if (now.before(task.getStartTime())) { return SignResult.error(签到尚未开始); } if (now.after(task.getEndTime())) { return SignResult.error(签到已结束); } // 2. 防重复以任务ID 学生ID查唯一记录 if (recordMapper.exists(task.getId(), studentId)) { return SignResult.error(你已完成签到请勿重复提交); } // 3. 迟到判定若任务配置了迟到阈值超过时间则打迟到标记 boolean late task.getLateThreshold() ! null now.after(DateUtils.addMinutes(task.getStartTime(), task.getLateThreshold())); // 4. 写记录 AttendanceRecord record new AttendanceRecord(); record.setTaskId(task.getId()); record.setStudentId(studentId); record.setDeviceId(deviceId); record.setSignTime(now); record.setSignType(2); record.setSignStatus(late ? 2 : 1); recordMapper.insert(record); return SignResult.success(late ? 签到成功但已迟到 : 签到成功); }这段代码的注意点第一步先校验时间第二步查重第三步算迟到第四步插入顺序不能反。有的人先插入再判断迟到结果记录已经落库了状态字段只改了一半。参数上deviceId是可选的如果uniapp端拿不到设备MAC安卓10以后拿不到真实MAC就传一个本地生成的UUID后端只做幂等辅助不依赖它做唯一约束。真正的唯一约束放在数据库的uk_task_student上代码里的exists只是为了给用户更快的提示。如果你要对“普通签到”增加验证码能力把签到码字段也放到任务表里校验放在时间窗口判断之前。3.3 位置签到与自动签到算距离、抓Wi-Fi、留兜底位置签到的服务端判定核心是球面距离计算。宿舍楼一般就那几栋允许半径设50到100米比较合理设太小学生在楼道里走动都会误判设太大隔壁楼的人也能签上。我一般把半径做成任务级别的可配置项不同楼栋用不同半径而不是在代码里写死。还有一种做法是分楼层设半径但宿舍考勤通常不需要那么精确因为查寝关心的是“人在这栋楼”不是“人在哪个房间”。public SignResult locationSign(AttendanceTask task, BigDecimal lat, BigDecimal lng) { if (!Integer.valueOf(3).equals(task.getTaskType())) { return SignResult.error(当前任务不支持位置签到); } // Haversine公式计算两个经纬度点的球面距离地球半径取6371000米 double earthRadius 6371000; double centerLat task.getLocationLat().doubleValue(); double centerLng task.getLocationLng().doubleValue(); double latDelta Math.toRadians(lat.doubleValue() - centerLat); double lngDelta Math.toRadians(lng.doubleValue() - centerLng); double a Math.sin(latDelta / 2) * Math.sin(latDelta / 2) Math.cos(Math.toRadians(centerLat)) * Math.cos(Math.toRadians(lat.doubleValue())) * Math.sin(lngDelta / 2) * Math.sin(lngDelta / 2); double distance earthRadius * 2 * Math.asin(Math.sqrt(a)); int radius task.getLocationRadius() null ? 100 : task.getLocationRadius().intValue(); if (distance radius) { return SignResult.success(位置签到成功); } return SignResult.error(不在允许范围当前距离中心点 (int) distance 米); }这段代码里重点是坐标系的统一。uniapp端返回的gcj02坐标国内高德、微信小程序默认就是GCJ02坐标系如果后台引用的地图组件或中心点坐标来自WGS84GPS原始坐标两者会有几百米偏差学生明明站在宿舍里也会被判“不在范围”。解决办法是建立位置签到任务时把宿舍楼的中心经纬度也用同一坐标系采集千万别混用。参数上location_radius的单位是米默认100location_lat和location_lng用DECIMAL(10,7)精度小数点后7位大约能精确到厘米级足够用了。我在实际项目里见过有人用DECIMAL(8,2)存经纬度结果精确到100米级别位置签到整个废掉。自动签到在宿舍场景里最常见的实现不是GPS而是Wi-Fi嗅探。理想状态是学生手机连上宿舍指定Wi-Fi后台通过AP的SSID判断他人在宿舍。但现实里有两个坑一是安卓和iOS都不允许网页或小程序直接读取Wi-Fi MACuniapp也读不到二是学生可以连上Wi-Fi但人不在宿舍比如隔壁宿舍用了同款SSID。所以我的建议是把自动签到做成“辅助判定”而不是“唯一判定”即自动签到成功后打一个“疑似在寝”的标记如果这个学生当晚没有其他任何操作由宿管在第二天抽检确认。兜底逻辑永远是GPS不行、Wi-Fi不行的时候允许学生提交手写确认后端打上“人工兜底”标签并记录操作人这样每个考勤结果都有明确来源。4. uniapp端实现Token怎么带、签到按钮怎么分状态4.1 请求封装与登录态过期处理uniapp端对接若依最核心的是把登录逻辑走通。若依默认的登录接口是/login通过账号密码换token。在uniapp这边我通常在common/request.js里封装一个全局请求方法把token缓存到uni.setStorageSync每次请求自动带上一旦接口返回401清掉token并跳转登录页。这里有一个容易忽略的点若依默认可能还开了验证码captcha登录前要先请求验证码接口拿到图片和uuid登录时一并传过去否则会一直卡在“验证码错误”。// 封装uniapp的请求自动携带Token并统一处理401 const request (options) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Authorization: Bearer ${uni.getStorageSync(token)}, Content-Type: application/json }, success: (res) { // 若依的响应结构默认是 code/msg/data if (res.data.code 200) { resolve(res.data.data); } else if (res.data.code 401) { // 401表示token失效清掉并回登录页 uni.removeStorageSync(token); uni.navigateTo({ url: /pages/login/index }); reject(res.data); } else { uni.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(res.data); } }, fail: (err) { uni.showToast({ title: 网络异常请检查网络, icon: none }); reject(err); } }); }); }; export default request;这里有几个细节值得说明BASE_URL不要写死在代码里我在项目里用一个config.js区分开发环境和生产环境小程序里uni.request的url要填完整地址不能像H5那样依赖相对路径所以BASE_URL在小程序端必须配成https的域名code 200的判断依据是若依默认的响应体结构如果你用的若依版本改了响应格式这行要跟着调整。另外401处理里要加一个防抖否则并发请求同时返回401时页面会被多次navigateTo踢回登录页我处理的方式是加一个isRedirecting的全局标记。4.2 签到按钮的状态管理未开始、进行中、已结束三种态签到页最容易被骂的体验问题是学生看到按钮能点点了之后弹“签到已结束”。原因是前端没有在页面加载时拉取任务状态只根据后端返回的错误弹窗。我在uniapp端用任务状态字段控制按钮的三态渲染并且每隔30秒轮询一次任务状态在onShow里启动、onHide里停止保证学生操作前就知道能不能签。// 考勤任务状态0未开始 1进行中 2已结束 export default { data() { return { task: null, alreadySigned: false, buttonText: 加载中, buttonDisabled: true }; }, onShow() { this.loadTask(this.taskId); this.timer setInterval(() this.loadTask(this.taskId), 30000); }, onHide() { clearInterval(this.timer); }, methods: { async loadTask(taskId) { const task await request({ url: /attendance/task/${taskId} }); this.task task; this.alreadySigned task.alreadySigned; if (task.status 0) { this.buttonText 签到未开始; this.buttonDisabled true; } else if (task.status 1) { this.buttonText this.alreadySigned ? 已完成签到 : 立即签到; this.buttonDisabled this.alreadySigned; } else { this.buttonText 签到已结束; this.buttonDisabled true; } }, handleSign() { if (this.task.taskType 1) { this.voiceConfirm(); } else if (this.task.taskType 2) { this.normalSign(); } else if (this.task.taskType 3) { this.locationSign(); } } } };状态管理的要点是alreadySigned字段要在页面每次显示的onShow里刷新不能只靠登录时拉一次。有的学生把App切后台半小时再回来页面还是旧的“立即签到”按钮一按就报重复。另一个坑是轮询接口不要和签到接口同时触发我实际开发时遇到过轮询任务返回“已结束”签到请求刚好插入一半记录表出现状态不一致的情况最后靠数据库唯一索引兜底。如果你担心轮询太频繁可以改成WebSocket推送但大部分学校宿舍的弱网环境撑不住长连接短轮询反而是最稳定的方案。4.3 口头点名的双向确认与学生端弹窗口头点名在学生端不是主动签到而是被动接收。做法是辅导员在若依Web端点击“开始点名”后台通过推送或定时轮询把任务状态带到学生端。很多学校内网环境不允许WebSocket长连接最简单可靠的方案是短轮询任务进行中学生端的App每隔10秒问一次“有没有我的点名确认”有就弹模态框。这个轮询可以和4.2里的任务状态轮询合并成一个接口减少请求次数。// 口头点名确认轮询到当前用户被点名时弹出确认卡片 async checkVoiceConfirm() { const res await request({ url: /attendance/voice/check, method: POST, data: { studentId: this.studentId } }); if (res.needConfirm) { uni.showModal({ title: 口头点名, content: ${res.taskName}请确认你在宿舍, success: (confirmRes) { if (confirmRes.confirm) { this.submitVoiceConfirm(res.taskId); } } }); } }这个交互里有个容易忽视的边界如果一个学生同时被两个任务点名比如晚点名和突击查寝同时进行前端要以task_id为key做去重弹窗只保留最新一个任务的确认卡片后端也要在submitVoiceConfirm接口里校验任务状态防止学生在任务结束后补确认。确认后记录表会有一条sign_type1的记录统计报表里要单独标记为“口头点名”不能和普通签到混在一起算出勤率。如果学生拒绝确认前端要调用一个“拒绝”接口后端记录理由让辅导员能看到是谁拒绝的方便线下核实这也是考勤可追溯的一部分。5. 考勤系统常见问题与排错五个高频翻车点5.1 服务器时区不一致迟到误判全乱套现象很典型后台统计某一天“迟到”人数暴增仔细看时间全是23:50左右。原因排查到最后是服务器时区是UTCnew Date()取出来的时间比北京时间晚8个小时晚间查寝正好踩在迟到阈值的临界点上。解决在若依后端的启动参数里固定时区比如在启动脚本的JAVA_OPTS里加上-Duser.timezoneAsia/Shanghai同时数据库连接串显式指定serverTimezoneAsia/Shanghai这样JDBC读出来的时间才和前端展示一致。改完以后用一条SQL把服务器当前时间和数据库时间对比确认两边差距在1秒内。这个坑最坑的地方在于本地开发时不会触发因为本地电脑时区一般是对的只有部署到云服务器上才暴露。5.2 位置签到永远差200米坐标系没统一现象学生在宿舍床上点位置签到提示“距离中心点285米”在走廊上签就成功在房间里就失败。原因很明确uniapp的uni.getLocation返回的是GCJ02坐标而后台任务配置中心点的时候用的是WGS84坐标很多网页端地图默认就是WGS84两种坐标系在同一个经纬度下偏差约200到500米。解决建立位置签到任务时从uniapp端采集楼栋中心点不要从Web端地图随便点一个点这样前后端坐标系天然一致。如果一定要用Web端配置后端需要写一个GCJ02转WGS84的转换方法或者干脆反过来所有距离计算统一用GCJ02。还有一个细节微信小程序的uni.getLocation返回的就是GCJ02文档里写得很清楚但很多开发者不看直接用原始值当GPS坐标算翻车之后还以为是接口的问题。5.3 二维码签到码被截图转发人不在也能签现象有人把普通签到的二维码截图发到宿舍群全宿舍不出门也能签上。原因很直白静态二维码只携带任务ID没有任何人、时间、设备绑定等于把门禁卡复印了发给大家。解决二维码做成动态的比如5秒刷新一次二维码内容里带一个随机nonce服务端同一nonce只能被使用一次。同时在这个任务里开启设备指纹校验同一个设备只能签一个学号再配合签到时间窗口和签到时的粗略位置基站ID或Wi-Fi信息双重校验。真正要防代签二维码只是入口判定还是靠后端组合条件。我还会在二维码页面把学生姓名打码显示减少二维码被拍照后直接分享的动机。5.4 弱网下连点两次一个人两条考勤记录现象宿舍楼道信号差学生点签到按钮没反应以为没点上又点一次后台出现两条记录。原因前端没做提交按钮防抖后端插入前虽然查了一遍exists但两次请求并发到达查询都返回不存在随后都执行了插入。解决记录表上加uk_task_student(task_id, student_id)联合唯一索引作为最后一层兜底。服务端保留“先查后插”是为了给出友好提示但并发安全必须交给数据库约束。前端在提交后立刻把按钮置灰并显示“签到中”收到成功响应后再改为“已完成”这样即使用户手快连点也不会发出重复请求。另外若依的接口层可以加一个简单的幂等拦截用Redis存taskId:studentId的已签标记设置30秒过期但这只能挡同一时刻的重复挡不了跨天的重复签到所以表上的唯一索引才是根上的方案。5.5 学生调宿后历史考勤归属错乱现象A同学从201室调到303室管理员在后台查询201室某个月的考勤记录时发现A的名字还在里面但再查个人考勤A变成了303室导出统计时两个宿舍都算了一遍。原因考勤记录表里没有存宿舍快照查询时实时关联了学生当前宿舍导致历史记录被“移动”到了新宿舍。这种问题在测试环境很难发现因为测试数据一般不会做调宿操作等开学宿舍大调整时才集中爆发。解决在考勤记录表冗余dorm_full_path字段记录签到那一刻学生的宿舍路径。后面不管学生怎么调宿历史考勤永远归属在当时的宿舍下。统计导出同理按冗余字段分组不要按当前部门分组。如果学校有宿管系统调宿操作要在若依里同步更新学生的部门节点同时保留变更日志方便追查“为什么这个学生这学期在201室的数据变少了”。6. 把考勤数据用起来一小时搞定导出和异常复核先说说导出。若依后台自带Excel注解和导出工具如果直接对着dorm_attendance_record表导出出来的数据对宿管毫无意义因为他们要的是一张“哪楼哪些人没签”的汇总表而不是签到流水。我会在导出接口上做一个转换按任务分组统计应到、实到、未到、迟到再用列表展示。若依的Excel注解加上自定义的ExcelUtil一分钟就能生成对应的模板。异常复核是所有考勤系统最后的一道防线。我用过最有效的一个技巧是“设备指纹碰撞检测”同一台设备在同一个签到任务里出现了两次而两个学生分属不同宿舍这基本就是代签。做法很简单导出一份按device_id分组的签到明细看到同一个设备ID对应多个student_id就拉出来人工核验。还有一种情况是同一个学号一天内用两个不同设备签到也要标记为可疑这可能是学生换了手机也可能是账号被盗用。-- 导出复核用SQL统计单次任务各宿舍的应到、实到和缺勤 SELECT dorm_full_path AS 宿舍, COUNT(DISTINCT CASE WHEN r.id IS NOT NULL THEN s.user_id END) AS 实到人数, COUNT(DISTINCT CASE WHEN s.status 1 AND r.id IS NULL THEN s.user_id END) AS 缺勤人数 FROM sys_user s LEFT JOIN dorm_attendance_record r ON r.student_id s.user_id AND r.task_id ? AND r.sign_status ! 3 WHERE s.dept_id ? GROUP BY dorm_full_path;这段SQL的含义左连接把宿舍学生和签到记录做一次关联条件里用task_id限制只看某一次任务。sign_status ! 3是为了排除那些被自动兜底逻辑标记为“暂无法确认”的记录。dept_id参数就是若依数据权限已经过滤后的部门ID这样导出的数据天然符合管理员的查看范围不会越权看到其他楼栋。s.status 1表示只统计在校状态正常的学生已经退宿或休学的学生不参与应到人数计算。做考勤系统这几年我最大的教训是签到功能本身不难难的是让数据经得起追问。口头点名、普通签到、位置签到这些模式上线前一定要让宿管拿真实场景各测一轮尤其是位置签到建议打印出学生签到坐标和宿舍中心点的偏差值肉眼确认半径是否合理。设备指纹碰撞和宿舍快照这两个设计都是在被真实的代签和调宿问题教育过之后才补上的如果你一开始就照着做能省下不少返工时间。希望这些踩坑记录和工程习惯能帮到你少走点弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑