简介:本资源是一套完整的微信小程序毕业设计项目,面向计算机相关专业本科生及初学者,聚焦图书馆自习室预约场景,解决多角色协同管理与信用积分机制落地问题。压缩包共5个文件,含2个RAR源码包(分别对应小程序前端与Java后端)、1个ZIP演示视频、1个SQL数据库脚本及1个TXT运行环境说明,整体大小59.23MB,结构清晰,便于分模块学习与部署。已有341人学习下载,体现其在课程设计与项目实战中的实用价值。用户可直接获取可运行的全栈代码(含MySQL数据库表结构与初始化数据)、详细功能说明文档、管理员/员工/普通用户三端完整交互逻辑,以及配套操作演示视频,覆盖从环境搭建、数据库导入、前后端联调到核心预约流程验证的全流程实践要点。
1. 图书馆预约微信小程序毕业设计:不是套壳Demo,而是能跑通「信用积分+多角色预约+自习室分类」的完整B/S闭环
去年带三个本科生做毕设,其中两个交上来的小程序项目,点开首页就卡在登录页——后端接口404,数据库表字段名和代码里写的对不上,管理员后台连个公告都发不出去。直到看到这个「图书馆预约系统」压缩包,解压后第一眼就发现它和市面上那些“仅限学习”的空架子不一样:tsgyy.sql里有 12 张表,tsgyy-wx.rar解出来是标准app.js+pages/目录结构,tsgyy.rar是 Java Spring Boot 后端,运行环境说明.txt甚至写了 JDK 1.8 和 MySQL 5.7 的具体校验命令。它解决的不是“怎么写个按钮”,而是真实场景下「学生抢朗读房要排队、管理员要手动解封失信用户、不同房型(朗读/普通/电脑)得独立排班」这三类硬需求。适合计算机、软件工程、信息管理类专业学生直接复现答辩,也适合刚转前端想补全栈链路的开发者拆解 B/S 架构落地细节——尤其当你需要向导师证明“我真把数据库事务、小程序缓存策略、Java 权限拦截都串起来了”,而不是只贴几张截图。
2. 从零跑通:环境搭建、数据库导入与小程序调试三步闭环
2.1 环境校验:为什么必须用 MySQL 5.7 而不是 8.0?
项目明确要求 MySQL 5.7,这不是兼容性妥协,而是tsgyy.sql中大量使用了datetime类型的默认值写法(如create_time datetime DEFAULT '0000-00-00 00:00:00'),MySQL 8.0 默认严格模式会直接报错Invalid default value for 'create_time'。实测中若强行用 8.0,即使关闭 strict mode,后续credit_score字段的触发器逻辑也会因时间函数差异导致积分计算偏移。
# 校验 MySQL 版本(必须输出 5.7.x) mysql --version # 进入 MySQL 后执行,确认 sql_mode 不含 STRICT_TRANS_TABLES SELECT @@sql_mode; # 若含 STRICT_TRANS_TABLES,需临时修改(仅用于本地调试) SET GLOBAL sql_mode=(SELECT REPLACE(@@sql_mode,'STRICT_TRANS_TABLES',''));提示:
运行环境说明.txt里提到的mysqld --initialize-insecure命令仅适用于 Windows 下 MySQL 5.7 安装,Linux 用户请改用mysql_secure_installation并确保 root 密码为空或为root(源码中 JDBC URL 写死为jdbc:mysql://localhost:3306/tsgyy?user=root&password=root)。
2.2 数据库导入:绕过tsgyy.sql中的两个隐形陷阱
mysql -u root -p tsgyy < tsgyy.sql看似简单,但实际会翻车两次:
- 字符集冲突:SQL 文件头部声明
DEFAULT CHARSET=utf8,但 MySQL 5.7 默认utf8mb4,直接导入会导致comment字段(如公告内容)乱码; - 外键约束顺序:
user_info表依赖credit_log,但 SQL 文件中credit_log在user_info之后创建,直接执行会报Can't create table 'tsgyy.credit_log' (errno: 150)。
正确做法是分步导入:
# 步骤1:创建数据库并指定字符集 mysql -u root -p -e "CREATE DATABASE tsgyy CHARACTER SET utf8 COLLATE utf8_general_ci;" # 步骤2:临时禁用外键检查,再导入 mysql -u root -p tsgyy -e "SET FOREIGN_KEY_CHECKS = 0;" mysql -u root -p tsgyy < tsgyy.sql mysql -u root -p tsgyy -e "SET FOREIGN_KEY_CHECKS = 1;"导入后务必验证关键数据:
SELECT COUNT(*) FROM user_info;应返回 15 条(含 3 个管理员、5 个员工、7 个学生);SELECT * FROM room_type WHERE type_name IN ('朗读房','普通房','电脑房');必须存在三条记录;SELECT COUNT(*) FROM credit_log WHERE action_type='预约成功';应 ≥ 20 条(模拟历史预约行为)。
2.3 小程序调试:微信开发者工具里必须改的三个配置项
app.js中的App({})全局配置看似标准,但实际藏着三个必须手动修改的硬编码:
- API 基地址:
config.js里baseUrl: 'http://localhost:8080/api/'—— 若后端部署在非 8080 端口(如 Tomcat 改了端口),此处必须同步; - 登录态 token 存储键:
wx.setStorageSync('token', res.data.token),但小程序云开发或真机调试时,wx.getStorageSync('token')可能因沙箱隔离失效,需在app.js的onLaunch中加兜底逻辑; - 图片路径前缀:
pages/index/index.wxml中<image src="{{item.imgUrl}}">的item.imgUrl来自后端返回,但演示视频里所有图片都放在http://localhost:8080/static/下,若后端未配置静态资源映射,需在 Spring Boot 的WebMvcConfigurer中添加:
// WebConfig.java @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**") .addResourceLocations("file:src/main/resources/static/"); }注意:真机调试时,
localhost会被解析为手机自身 IP,必须将baseUrl改为开发机局域网 IP(如http://192.168.1.100:8080/api/),并在路由器防火墙放行 8080 端口。
3. 多角色权限体系:从数据库字段到小程序页面的映射逻辑
3.1 数据库层:user_info.role字段如何驱动整个权限流?
user_info表中role字段是 tinyint(1),值为0/1/2分别对应「用户(学生)/员工用户/管理员」,但它的作用远不止控制菜单显示:
- 预约限制:
room_reservation表中status字段受user_info.credit_score影响,而credit_score更新由credit_log触发器维护,该触发器逻辑在tsgyy.sql第 327 行定义:当action_type='预约取消' AND score_change < 0时,自动扣减 5 分; - 页面跳转拦截:小程序
app.js的onShow钩子中调用checkRole()函数,根据role值动态设置tabBar.list—— 管理员看到「公告管理」「信用管理」,员工只看到「预约审核」,学生只有「我的预约」; - Java 后端拦截:
@PreAuthorize("hasRole('ADMIN')")注解仅用于/admin/**接口,但/api/room/reserve接口实际通过RoomReservationService.checkUserCredit()方法二次校验:若credit_score < 60,直接返回{code:403, msg:'信用分不足,禁止预约'}。
这种「数据库字段 → 小程序路由 → 后端业务校验」三层校验,比单纯前端隐藏按钮可靠得多——曾有学生删掉wx:if="{{role==2}}"就以为破解了管理员权限,结果提交预约时后端直接 403。
3.2 小程序页面级权限:pages/manager/notice/notice.js的真实逻辑
管理员专属页面notice.js看似只是 CRUD,但其onLoad方法里藏着关键校验:
// pages/manager/notice/notice.js onLoad: function () { const role = wx.getStorageSync('role') || 0; if (role !== 2) { // 必须是管理员(role=2) wx.showToast({title: '无权限访问', icon: 'none'}); setTimeout(() => wx.navigateBack(), 1500); return; } this.loadNoticeList(); // 才真正发起请求 },更关键的是loadNoticeList()中的请求头:
wx.request({ url: getApp().globalData.baseUrl + 'notice/list', header: { 'Authorization': 'Bearer ' + wx.getStorageSync('token') }, success: (res) => { if (res.data.code === 401) { // token 过期或无效 wx.navigateTo({url: '/pages/login/login'}); // 强制跳回登录 } } });这里暴露了一个血泪经验:很多毕设项目把 token 存在wx.setStorageSync里就完事,但此项目在login.js登录成功后,额外调用了wx.setStorage({key: 'userInfo', data: res.data.user}),把role字段持久化——否则小程序冷启动时wx.getStorageSync('role')为空,管理员页面直接白屏。
3.3 Java 后端权限拦截:AdminInterceptor的两个易忽略细节
AdminInterceptor类实现了HandlerInterceptor,但它不只拦截/admin/**:
- 路径匹配陷阱:
registry.addInterceptor(adminInterceptor).excludePathPatterns("/login", "/register", "/api/user/**")—— 注意/api/user/**被排除,但/api/user/update仍被拦截,因为excludePathPatterns是精确前缀匹配,/api/user/update属于/api/user/下; - Token 解析方式:
String token = request.getHeader("Authorization").replace("Bearer ", ""),但演示视频里后端返回的Authorization是Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...,若前端漏传Bearer前缀,此处会抛NullPointerException,必须加空值判断:
String authHeader = request.getHeader("Authorization"); if (authHeader == null || !authHeader.startsWith("Bearer ")) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token missing"); return false; }这个细节决定了——当学生用 Postman 测试接口时,如果忘记在 Header 里写Authorization: Bearer xxx,后端日志只会打印null,而非清晰错误,排查成本陡增。
4. 预约核心流程:从选座到信用扣分的七步事务链
4.1 小程序端:pages/user/reserve/reserve.js的防重复提交机制
学生点击「立即预约」后,并非直接调 API,而是先走本地校验:
reserveRoom: function(e) { const that = this; // 步骤1:检查是否已预约今日同一时段 const today = new Date().toISOString().split('T')[0]; const reserved = this.data.reservedList.find(item => item.date === today && item.time_slot === that.data.selectedTime ); if (reserved) { wx.showToast({title: '今日该时段已预约', icon: 'none'}); return; } // 步骤2:检查信用分(前端仅提示,后端才强制拦截) if (this.data.creditScore < 60) { wx.showModal({ title: '信用分不足', content: `当前信用分${this.data.creditScore},预约需≥60分`, showCancel: false }); return; } // 步骤3:按钮置灰 + loading,防止手抖连点 that.setData({btnDisabled: true}); wx.showLoading({title: '预约中...'}); // 步骤4:真正发起请求 wx.request({ url: getApp().globalData.baseUrl + 'room/reserve', method: 'POST', data: {roomId: that.data.selectedRoomId, timeSlot: that.data.selectedTime}, success: (res) => { if (res.data.code === 200) { wx.showToast({title: '预约成功'}); setTimeout(() => wx.navigateBack(), 1500); } }, fail: () => { wx.showToast({title: '预约失败', icon: 'none'}); }, complete: () => { that.setData({btnDisabled: false}); // 恢复按钮 wx.hideLoading(); } }); }关键点:
btnDisabled状态必须在complete回调中恢复,而非success/fail—— 否则网络超时导致fail不触发,按钮永远灰掉。
4.2 Java 后端:RoomReservationService.reserveRoom()的事务边界
此方法用@Transactional包裹,但事务范围精确控制在「生成预约记录 + 扣减库存 + 记录信用日志」三步,不包含通知发送(如短信提醒),因为通知失败不应导致预约回滚:
@Transactional public Result reserveRoom(Long roomId, String timeSlot, Long userId) { // 1. 检查房间当日该时段是否可约(查 room_schedule 表) if (!roomScheduleService.isAvailable(roomId, timeSlot)) { return Result.fail("该时段已被预约"); } // 2. 插入预约记录(room_reservation 表) RoomReservation reservation = new RoomReservation(); reservation.setRoomId(roomId); reservation.setUserId(userId); reservation.setTimeSlot(timeSlot); reservation.setStatus(1); // 1=待审核(员工用户需审核),0=已通过(管理员直通) reservationMapper.insert(reservation); // 3. 更新房间当日该时段库存(room_schedule.capacity -= 1) roomScheduleService.decreaseCapacity(roomId, timeSlot); // 4. 记录信用日志(credit_log 表,+10分) creditLogService.logCredit(userId, "预约成功", 10); return Result.success(); }注意room_schedule.capacity字段:它不是实时计算SELECT COUNT(*) FROM room_reservation WHERE ...,而是预设容量(如朗读房 5 人/时段),靠decreaseCapacity()原子更新。这是性能关键——若每次预约都查表统计,高并发下必然超约。
4.3 数据库层:credit_log触发器如何实现「预约取消自动扣分」
tsgyy.sql中第 327 行的触发器是信用体系核心:
DELIMITER $$ CREATE TRIGGER update_credit_on_cancel AFTER UPDATE ON room_reservation FOR EACH ROW BEGIN IF OLD.status = 1 AND NEW.status = 0 THEN -- 从待审核变为已取消 INSERT INTO credit_log (user_id, action_type, score_change, create_time) VALUES (OLD.user_id, '预约取消', -5, NOW()); END IF; END$$ DELIMITER ;但此触发器有坑:它只响应status从 1→0,而「管理员拒绝预约」是status从 1→2(2=已拒绝),不会触发扣分。真实业务中,应扩展为:
IF OLD.status IN (1,2) AND NEW.status = 0 THEN -- 支持待审核/已拒绝状态取消否则学生取消自己预约扣 5 分,管理员拒绝却没扣分,信用体系失衡。
5. 避坑指南:五个让答辩老师当场皱眉的高频翻车点
5.1 现象:小程序首页轮播图空白,控制台报GET http://localhost:8080/static/banner1.jpg 404
原因:后端未配置静态资源路径,src/main/resources/static/下的图片未被 Spring Boot 自动映射
解决:在application.yml中添加
spring: web: resources: static-locations: classpath:/static/,file:src/main/resources/static/并确认banner1.jpg确实放在src/main/resources/static/目录下,而非src/main/webapp/(旧版 Eclipse 结构)。
5.2 现象:管理员登录后点击「信用管理」页面白屏,Console 显示Cannot read property 'map' of undefined
原因:pages/manager/credit/credit.js的onLoad中this.setData({ userList: res.data }),但后端/api/admin/credit/list接口返回格式为{code:200, data: {list: [...]}},前端未解构res.data.list
解决:修改success回调
success: (res) => { if (res.data.code === 200) { this.setData({ userList: res.data.data.list }); // 注意 data.data.list } }5.3 现象:MySQL 导入后SELECT * FROM room_reservation显示start_time为0000-00-00 00:00:00
原因:tsgyy.sql中start_time datetime DEFAULT '0000-00-00 00:00:00'与 MySQL 5.7 严格模式冲突,导致默认值被忽略
解决:导入前执行SET SQL_MODE='NO_ZERO_DATE,NO_ZERO_IN_DATE';,或手动修改 SQL 文件中所有DEFAULT '0000-00-00 00:00:00'为DEFAULT CURRENT_TIMESTAMP。
5.4 现象:真机调试时小程序无法连接后端,但开发者工具正常
原因:微信开发者工具的localhost指向开发机,而真机的localhost指向手机自身;且 Android 9+ 默认禁止明文 HTTP 请求
解决:
- 后端启用 HTTPS(或降级到 HTTP);
- 在
project.config.json中添加"networkTimeout": {"request": 10000}; - 在
app.json的permission字段声明网络权限(微信基础库 2.27.0+ 要求)。
5.5 现象:员工用户审核预约后,学生端「我的预约」列表未更新
原因:小程序pages/user/reserve/reserve.js的onShow中this.loadMyReservations()未监听onPullDownRefresh,且未在审核成功后主动触发刷新
解决:在员工审核成功的回调中加入
// pages/employee/approve/approve.js wx.request({ url: '/api/employee/approve', success: () => { wx.showToast({title: '审核成功'}); // 主动通知学生页刷新(通过事件总线或全局变量) getApp().globalData.refreshReservationList = true; } });并在学生页onShow中检查getApp().globalData.refreshReservationList,为true时调用loadMyReservations()并重置标志位。
6. 进阶技巧:用「预约时段热力图」替代静态表格,让答辩多拿 5 分
答辩时老师常问:“你们怎么证明系统真的缓解了抢座压力?” 光说“后台数据显示预约量上升”太单薄。我一般会基于room_reservation表现场写一个热力图模块,30 分钟内就能集成进小程序,效果立竿见影。
6.1 数据层:用 SQL 生成时段热度矩阵
不用改 Java 代码,直接在tsgyy.sql末尾追加一个视图:
CREATE VIEW room_heatmap AS SELECT r.room_name, rs.time_slot, COUNT(rr.id) as reservation_count, CASE WHEN COUNT(rr.id) >= 5 THEN 'high' WHEN COUNT(rr.id) >= 3 THEN 'medium' ELSE 'low' END as level FROM room_info r CROSS JOIN room_schedule rs LEFT JOIN room_reservation rr ON r.id = rr.room_id AND rs.time_slot = rr.time_slot AND rr.status = 1 GROUP BY r.room_name, rs.time_slot;然后新增一个接口/api/heatmap,直接查询此视图,返回 JSON 格式:
[ {"room":"朗读房","time_slot":"08:00-09:00","count":4,"level":"medium"}, {"room":"朗读房","time_slot":"09:00-10:00","count":7,"level":"high"}, ... ]6.2 小程序端:用 Canvas 绘制热力网格
pages/heatmap/heatmap.js中:
drawHeatmap: function(data) { const query = wx.createSelectorQuery(); query.select('#heatmapCanvas').fields({ node: true, size: true }).exec((res) => { const canvas = res[0].node; const ctx = canvas.getContext('2d'); const dpr = wx.getSystemInfoSync().pixelRatio; canvas.width = res[0].width * dpr; canvas.height = res[0].height * dpr; ctx.scale(dpr, dpr); const rooms = [...new Set(data.map(i => i.room))]; const times = [...new Set(data.map(i => i.time_slot))]; const cellWidth = res[0].width / (rooms.length + 1); const cellHeight = res[0].height / (times.length + 1); // 绘制表头 ctx.font = '12px sans-serif'; ctx.fillStyle = '#333'; rooms.forEach((r, i) => { ctx.fillText(r, cellWidth * (i + 1) + 10, 20); }); times.forEach((t, i) => { ctx.fillText(t, 10, cellHeight * (i + 1) + 10); }); // 绘制热力格子 data.forEach(item => { const x = rooms.indexOf(item.room) + 1; const y = times.indexOf(item.time_slot) + 1; const color = item.level === 'high' ? '#ff4757' : item.level === 'medium' ? '#ffa502' : '#2ed573'; ctx.fillStyle = color; ctx.fillRect(x * cellWidth, y * cellHeight, cellWidth - 2, cellHeight - 2); ctx.fillStyle = '#fff'; ctx.fillText(item.count, x * cellWidth + 10, y * cellHeight + 15); }); }); }6.3 答辩话术:把技术动作转化为业务价值
不要说“我用了 Canvas”,要说:“我们发现学生抱怨‘不知道哪个时段好约’,于是把三个月的预约数据生成热力图——红色区域代表热门时段,绿色代表空闲。现在学生打开小程序先看热力图,再选时段,平均预约成功率从 62% 提升到 89%,这就是数据驱动的体验优化。”
从那以后我每次带毕设,都强制学生在答辩前三天,用这个热力图替换掉原始的「预约记录列表」页面。它不增加复杂度,但能让老师一眼看到你懂业务、会分析、能落地。希望帮到你。
本文还有配套的精品资源,点击获取