简介:这份资源是面向高校宿管人员与学生、以及微信小程序开发初学者的宿舍管理系统完整项目源码,基于微信开发者工具构建,采用双端登录设计,解决传统宿舍管理中签到、卫生评分、留言沟通与晚归申请等事务效率低下的问题。压缩包共2000个文件,约8.4MB,以769个js与393个ts脚本承载核心业务逻辑,258个json负责配置,236个md提供说明文档,另有wxml、wxss构建页面样式,并包含proto、schema等接口定义文件,整体结构完整、便于二次开发。目前已有1956人学习下载。项目涵盖GPS定位签到、宿管端卫生评分与留言回复、学生端晚归申请等功能模块,并预留MYSQL数据库接口用于密码校验与数据存储,读者可据此掌握小程序双角色权限设计、前后端数据交互与数据库对接思路,适合作为课程设计或毕业设计的参考方案。
1. 宿舍管理系统为什么值得用微信小程序重做一遍
很多高校的宿舍管理还停留在 Excel 加微信群通知的阶段:宿管阿姨在群里发一条「今晚查寝」,学生回「收到」,数据落在聊天记录里,月底统计时再一条条翻。这套流程在 200 人以内还能凑合,一旦超过 500 人,漏报、错报、查不到历史记录就成了常态。基于微信小程序的宿舍管理系统,本质上是把「查寝签到、报修登记、晚归请假、水电费公示」这几件高频小事,从聊天记录里搬到有结构、可查询、能导出报表的数据库里。
它适合三类人:一是想做一个能写进简历、又能真实跑起来的全栈练手项目的学生;二是被纸质登记表折磨、想自己动手改善流程的宿管或辅导员;三是需要一套轻量级 CRUD 加权限控制范例的开发者。微信小程序的优势在于免安装、扫码即用、天然带用户身份(openid),省掉了注册登录这一大块开发量。下面我从数据建模讲到接口实现,再到部署上线,把这条链路完整走一遍。
2. 先定角色和数据表:别急着写页面
2.1 三种角色与权限边界
宿舍管理系统的角色划分直接决定表结构和接口鉴权方式。常见做法是分三种:学生、宿管(楼长)、管理员。学生只能看自己的信息、提交报修和请假;宿管能看本楼栋所有学生的签到状态、审批请假;管理员管楼栋、房间、账号的增删改查。
权限不能只在前端判断,否则改个请求参数就能越权。我的做法是在数据库的用户表里存一个role字段,后端每个接口先查当前 openid 对应的角色,再决定是否放行。下面这张表是我一般会用的角色权限对照:
| 角色 | role 值 | 可访问接口 | 数据范围 |
|---|---|---|---|
| 学生 | student | 签到、报修、请假、查水电 | 仅本人 |
| 宿管 | manager | 查本楼签到、审批请假、处理报修 | 本楼栋 |
| 管理员 | admin | 楼栋/房间/账号管理、导出报表 | 全部 |
2.2 核心数据表设计与建表语句
表设计是整个系统的地基,改表比改代码痛苦得多。我踩过的坑是:一开始把「房间号」直接存成字符串放在学生表里,后来要按楼栋统计时发现没法聚合。正确做法是把楼栋、房间拆成独立表,学生表只存外键。
-- 楼栋表 CREATE TABLE building ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL COMMENT '楼栋名称,如1号楼', manager_openid VARCHAR(64) COMMENT '该楼宿管的openid' ); -- 房间表 CREATE TABLE room ( id INT PRIMARY KEY AUTO_INCREMENT, building_id INT NOT NULL, room_no VARCHAR(16) NOT NULL COMMENT '房间号,如302', capacity TINYINT DEFAULT 4 COMMENT '床位容量', INDEX idx_building (building_id) ); -- 用户表 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) UNIQUE NOT NULL, name VARCHAR(32), role ENUM('student','manager','admin') DEFAULT 'student', room_id INT, INDEX idx_room (room_id) ); -- 签到记录表 CREATE TABLE checkin ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, check_date DATE NOT NULL, status ENUM('normal','late','absent') DEFAULT 'normal', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_date (user_id, check_date) );逻辑说明:checkin表用(user_id, check_date)做唯一索引,这样同一天重复签到会被数据库直接拦掉,不用在代码里写一堆判断。status用枚举而不是数字,查询时一眼能看懂。参数上,capacity用 TINYINT 够用,一个房间不会超过 127 个床位。
注意:openid 长度不固定,别用 CHAR(28) 这种定长,用 VARCHAR(64) 留足余量。
2.3 为什么不用云开发的默认权限
微信云开发提供了「仅创建者可读写」这类默认权限,看起来省事,但宿舍管理是典型的多角色交叉访问:宿管要读全楼学生的签到,学生只能读自己的。默认权限满足不了这种需求,最后还是得在云函数里自己写鉴权。所以我一般直接用云数据库加云函数,把权限逻辑收在云函数里,前端只负责展示。
3. 用云函数把签到和报修接口跑通
3.1 初始化项目与目录结构
先在微信开发者工具里新建一个「云开发」项目,开通云环境后会得到一个环境 ID。目录我习惯这样分:
miniprogram/ pages/ checkin/ 签到页 repair/ 报修页 leave/ 请假页 utils/ request.js 统一请求封装 cloudfunctions/ login/ 获取openid checkin/ 签到逻辑 repair/ 报修逻辑request.js里统一封装wx.cloud.callFunction,把错误码和 loading 处理掉,页面里就不用每次都写 try-catch。
3.2 获取 openid 并写入用户表
小程序端拿不到 openid,必须通过云函数。下面这个 login 云函数是整套系统的入口:
// cloudfunctions/login/index.js const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() exports.main = async (event) => { // 从调用上下文里拿 openid,不信任前端传参 const { OPENID } = cloud.getWXContext() const users = db.collection('user') const exist = await users.where({ openid: OPENID }).get() if (exist.data.length === 0) { // 首次进入,建一条学生记录,room_id 待管理员分配 await users.add({ data: { openid: OPENID, role: 'student', room_id: null, name: '' } }) return { openid: OPENID, role: 'student', isNew: true } } return { openid: OPENID, role: exist.data[0].role, isNew: false } }逻辑说明:cloud.getWXContext()返回的 OPENID 由微信服务端注入,前端伪造不了,这是整个鉴权链的信任根。参数上,cloud.DYNAMIC_CURRENT_ENV让云函数自动使用当前环境,不用硬编码环境 ID。返回isNew是为了让前端知道要不要跳转到「完善信息」页。
3.3 签到接口与防重复提交
签到接口要处理三件事:判断今天是否已签到、判断是否晚归、写入记录。
// cloudfunctions/checkin/index.js const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() const _ = db.command exports.main = async (event) => { const { OPENID } = cloud.getWXContext() const today = new Date().toISOString().slice(0, 10) // YYYY-MM-DD // 晚归判定:22:30 之后算晚归 const now = new Date() const isLate = now.getHours() > 22 || (now.getHours() === 22 && now.getMinutes() >= 30) const user = await db.collection('user').where({ openid: OPENID }).get() if (!user.data.length) return { code: 401, msg: '用户不存在' } const userId = user.data[0]._id const exist = await db.collection('checkin') .where({ user_id: userId, check_date: today }).get() if (exist.data.length) return { code: 409, msg: '今日已签到' } await db.collection('checkin').add({ data: { user_id: userId, check_date: today, status: isLate ? 'late' : 'normal', created_at: db.serverDate() } }) return { code: 0, msg: '签到成功', status: isLate ? 'late' : 'normal' } }逻辑说明:db.serverDate()用服务端时间,避免学生改手机时间作弊。晚归阈值 22:30 写死在代码里,实际项目建议放到配置表,方便不同楼栋设不同时间。返回码用 0 表示成功、409 表示冲突,前端根据 code 弹不同提示。
参数上,check_date存成字符串YYYY-MM-DD而不是 Date 对象,因为云数据库对 Date 的时区处理容易出玄学问题,存字符串反而稳定。
3.4 报修接口与图片上传
报修比签到多一个图片上传环节。小程序端先用wx.chooseMedia选图,再wx.cloud.uploadFile传到云存储,拿到 fileID 后连同文字描述一起提交。
// miniprogram/pages/repair/repair.js Page({ data: { desc: '', fileID: '' }, chooseImg() { wx.chooseMedia({ count: 1, mediaType: ['image'], success: async (res) => { const tempPath = res.tempFiles[0].tempFilePath const upload = await wx.cloud.uploadFile({ cloudPath: `repair/${Date.now()}.jpg`, filePath: tempPath }) this.setData({ fileID: upload.fileID }) } }) }, async submit() { if (!this.data.desc) return wx.showToast({ title: '请填写描述', icon: 'none' }) const res = await wx.cloud.callFunction({ name: 'repair', data: { desc: this.data.desc, fileID: this.data.fileID } }) if (res.result.code === 0) { wx.showToast({ title: '提交成功' }) } } })逻辑说明:cloudPath用时间戳命名,避免同名覆盖。fileID 是云存储的唯一标识,存进数据库后,前端用wx.cloud.getTempFileURL换成临时链接展示。参数上,count: 1限制一次一张,如果要做多图,改成数组循环上传即可。
4. 避坑与排查:那些让我返工三次的问题
4.1 云函数超时导致签到失败
现象:晚高峰签到,部分学生提示「签到失败」,云函数日志显示 timeout。
原因:默认云函数超时是 3 秒,签到接口里串行查了用户表又查签到表,两次数据库往返加上冷启动,偶尔超过 3 秒。
解决:把两次查询改成一次聚合,或者在config.json里把超时调到 10 秒。我一般两个都做,查询用db.collection('user').aggregate()一次拿到用户和签到状态。
4.2 openid 在前端被当成普通参数传递
现象:有人把 openid 存在本地缓存,请求时当参数传给云函数,结果换个账号改缓存就能看别人数据。
原因:前端传的 openid 完全可伪造。
解决:所有云函数一律用cloud.getWXContext().OPENID,永远不读event.openid。这条是红线,我在 code review 里见到一次打回一次。
4.3 日期时区导致签到记录错位
现象:学生凌晨 0 点签到,记录却算到前一天。
原因:云函数运行环境是 UTC 时区,new Date().toISOString()拿到的是 UTC 日期,比北京时间晚 8 小时。
解决:手动加 8 小时偏移,或者用new Date(Date.now() + 8 * 3600 * 1000).toISOString().slice(0, 10)。这个坑很隐蔽,测试时白天测不出来,只有跨零点才暴露。
4.4 数据库权限设成「所有人可读」
现象:上线后发现有人能直接读全表数据。
原因:云数据库默认权限没改,还是「所有用户可读」。
解决:进云开发控制台,把每张表的权限改成「仅管理端可读写」,所有读写都走云函数。前端直连数据库虽然方便,但权限粒度太粗,不适合这种多角色系统。
4.5 图片上传没限制大小导致流量超标
现象:月底收到云存储流量告警。
原因:学生上传原图,一张 5MB,几百人一天传几十张。
解决:wx.chooseMedia里加sizeType: ['compressed'],并在云函数里校验 fileID 对应的文件大小,超过 2MB 直接拒绝。压缩后一般 200KB 以内,流量降一个数量级。
5. 把系统真正跑起来:部署、验证与一个提效技巧
5.1 上线前的三步验证
功能写完不等于能用,我一般按这三步验证:
第一步,用两个不同微信号分别登录,确认学生 A 看不到学生 B 的签到记录。这一步验证的是鉴权,最容易出问题。
第二步,把手机时间改到 23:00,签到一次,确认状态是late而不是normal。这一步验证的是晚归判定逻辑。
第三步,在云开发控制台手动删掉一条签到记录,再让该学生签到,确认能成功。这一步验证的是唯一索引和重复提交判断是否一致。
5.2 用定时触发器自动生成未签到名单
查寝最费时间的不是签到本身,而是事后统计谁没签。云开发支持定时触发器,可以每天 23:00 自动跑一个云函数,把当天没签到的人拉出来,推送给宿管。
// cloudfunctions/dailyReport/index.js const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() exports.main = async () => { const today = new Date(Date.now() + 8 * 3600 * 1000) .toISOString().slice(0, 10) // 拉全部学生 const students = await db.collection('user') .where({ role: 'student' }).get() // 拉今日已签到 const checked = await db.collection('checkin') .where({ check_date: today }).get() const checkedIds = new Set(checked.data.map(i => i.user_id)) const absent = students.data.filter(s => !checkedIds.has(s._id)) // 按楼栋分组后写入报表集合,宿管端读取 await db.collection('daily_report').add({ data: { date: today, absent: absent.map(s => s._id), count: absent.length } }) return { date: today, absentCount: absent.length } }逻辑说明:Set用来做 O(1) 查找,比数组includes快。参数上,定时触发器在config.json里配置 cron 表达式0 0 23 * * * *,表示每天 23:00 触发。注意云函数的 cron 用的是 UTC 时间,要减 8 小时,所以实际写0 0 15 * * * *。
5.3 一个让我少写一半代码的习惯
做完这个系统我最大的体会是:别急着写页面。先把角色、表结构、接口返回码定死,写成一份简单的接口文档,哪怕只有自己看。我前两次返工都是因为表结构没想清楚就动手,写到一半发现要加字段,前端跟着全改。后来我养成习惯,任何 CRUD 项目先花半天画表和定接口,后面写代码基本是填空。这个系统从建表到上线,真正写业务代码只用了两个晚上,大部分时间花在验证权限和时区这些边界上。希望帮到你。
本文还有配套的精品资源,点击获取