简介:本资源是一套完整的微信小程序毕业设计项目——基于Java SSM框架与UniApp/原生小程序开发的图书馆自习室预约系统,面向计算机专业本科生及课程设计学习者,解决高校图书馆座位资源紧张、人工管理低效、学生预约体验差等实际问题。压缩包共8个文件,含2份Word文档(开题报告与毕业论文)、1份PPT答辩稿、1个SQL数据库脚本、1个TXT说明文件、2个RAR格式前后端源码包(tsgyy-wx为小程序端,tsgyy为Java后台),整体42.69MB,结构清晰、模块完整,便于快速部署与二次开发。已有67人学习下载,涵盖前台公告浏览、自习室分类查看、信用分驱动的预约机制(含超时扣分逻辑)、留言板交互,以及后台公告管理、多类型房间配置、预约全周期监管与信用阈值控制等核心功能,配套文档齐全,可直接用于毕设答辩与系统复现。
1. 这不是又一个“Hello World”小程序:它是一套能直接跑通、带信用分逻辑和人工干预闭环的图书馆预约系统,专为毕业设计答辩卡点前72小时救急而生
你是不是也经历过——开题刚过,导师甩来一句“要体现业务闭环”,结果翻遍 GitHub 找到的全是“登录+列表+跳转”的空壳 demo?这个资源不是教学玩具,它在 MySQL 里真有credit_score字段、在 Java 后台真有CreditScoreService.updateByAdmin()方法、在小程序端真有“信用分低于60禁止预约”的弹窗拦截逻辑。它用 SSM 搭建后台(非 Spring Boot 简化版),用原生小程序 + 少量 uni-app 兼容写法实现前台,所有接口都带真实参数校验(比如预约时间不能早于当前时间30分钟、同一用户同一天不能预约超过2间)。数据库含 12 张表,从room_category(朗读房/电脑房分类)到reservation_audit_log(管理员人工扣分留痕日志)全链路覆盖。适合需要快速交付、能讲清“为什么用 SSM 而不用 Spring Boot”“信用分怎么防刷”“预约冲突怎么检测”的本科生,也适合想拿它改造成自习室共享平台、考研教室调度系统的课程设计者。别被标题里的“毕业设计”劝退——它的权限模型、时间校验粒度、人工审核留痕,比很多小公司线上系统还扎实。
2. 从解压到首页渲染:5 分钟跑通前后端联调,关键在三处路径配置和两个数据库初始化动作
2.1 后端部署:Tomcat 7 + JDK 1.8 的硬性组合,不是兼容问题而是设计约束
这个项目明确要求 Tomcat 7 和 JDK 1.8,不是作者偷懒,而是 SSM 框架中spring-webmvc-4.3.x与servlet-api-3.0的版本锁死关系。若强行升级到 Tomcat 9 或 JDK 11,DispatcherServlet初始化会抛java.lang.NoSuchMethodError: javax.servlet.http.HttpServletRequest.getHttpServletMapping()。正确操作是:
# 下载并解压 apache-tomcat-7.0.109.zip 到 /opt/tomcat7 # 设置 JAVA_HOME 指向 jdk1.8.0_202(必须是 1.8u202 及以下,u211+ 有 TLS 协议变更) export JAVA_HOME=/opt/jdk1.8.0_202 export PATH=$JAVA_HOME/bin:$PATH提示:JDK 1.8u202 是最后一个默认启用 TLSv1.1 的版本,而 Tomcat 7.0.109 的
catalina.properties中sslEnabledProtocols=TLSv1,TLSv1.1依赖此行为。换更高版本 JDK 会导致 HTTPS 管理后台打不开。
将tsgyy.rar解压后得到tsgyy目录,复制整个目录到tomcat7/webapps/下。注意不是 WAR 包,是已解压的 WebRoot 结构。启动前必须修改tsgyy/WEB-INF/classes/jdbc.properties:
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/tsgyy?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8 jdbc.username=root jdbc.password=123456这里serverTimezone=GMT%2B8是 MySQL 5.7 的强制要求,漏写会导致java.sql.SQLException: The server time zone value 'XXX' is unrecognized。jdbc.driver必须用com.mysql.jdbc.Driver(MySQL 5.x 驱动),不能写成com.mysql.cj.jdbc.Driver(那是 8.x 驱动,会报ClassNotFoundException)。
2.2 数据库初始化:tsgyy.sql不是单文件导入,而是分三步执行的结构迁移脚本
tsgyy.sql文件实际包含三部分:建库语句、建表语句、初始数据插入。但直接在 Navicat 11 中执行会失败——因为建表语句里有ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci,而 MySQL 5.7 默认字符集是utf8(即utf8mb3),不支持utf8mb4_unicode_ci排序规则。必须先手动创建数据库并指定字符集:
-- 在 Navicat 中新建查询,执行以下三行(不要复制整个 tsgyy.sql!) CREATE DATABASE tsgyy CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE tsgyy; -- 此时再右键点击数据库 → “运行 SQL 文件” → 选择 tsgyy.sql注意:
tsgyy.sql中第 87 行INSERT INTO room_category (id, name, description) VALUES (1, '朗读房', '允许轻声朗读');是唯一带中文注释的插入语句,若建库时未用utf8mb4,此处会报Incorrect string value: '\xE8\xAF\xBB\xE6\x88\xBF'。这是血泪经验——我曾因跳过建库步骤直接导入,花了 2 小时查编码,最后发现是 Navicat 默认建库用utf8。
2.3 小程序端启动:HBuilder X 导入tsgyy-wx.rar后,必须重写utils/request.js中的 base_url
tsgyy-wx.rar解压后是标准的小程序目录结构,但app.js中的base_url写的是http://localhost:8080/tsgyy/,这在真机调试时必然失败。必须修改utils/request.js:
// utils/request.js 第 5 行 const BASE_URL = 'http://192.168.1.100:8080/tsgyy/'; // 改为你的开发机局域网IP // 注意:不能写 localhost 或 127.0.0.1,真机无法解析 // 也不能写公网IP(除非你开了端口映射),局域网IP最稳然后在微信开发者工具中,点击“详情” → 勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。这是开发阶段必需操作,否则wx.request会直接返回request:fail net::ERR_CONNECTION_REFUSED。启动后首页显示“公告列表”和“自习室区域”,说明联调成功。
3. 信用分机制拆解:从数据库字段到人工扣分弹窗,看懂这套“可审计、可回滚”的设计逻辑
3.1 数据库层:user表的credit_score字段不是简单数字,而是带状态快照的审计字段
user表结构中,credit_score是INT(11)类型,初始值为 100。但关键不在字段本身,而在关联的credit_score_log表:
CREATE TABLE `credit_score_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL, `before_score` int(11) NOT NULL, `after_score` int(11) NOT NULL, `change_type` tinyint(4) NOT NULL COMMENT '1:预约未退出扣分, 2:管理员扣分, 3:管理员加分', `operator_id` bigint(20) DEFAULT NULL COMMENT '操作人ID(管理员ID)', `remark` varchar(255) DEFAULT NULL, `create_time` datetime NOT NULL, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意:
change_type字段用tinyint而非enum,是为了便于后期扩展(比如加“举报核实加分”类型)。operator_id允许为空,因为“预约未退出扣分”是系统自动触发,无需操作人;而管理员操作必须记录operator_id,这是答辩时证明“人工干预可追溯”的核心证据。
3.2 后台服务层:CreditScoreService的双重校验逻辑,防刷关键在@Transactional和时间窗口
CreditScoreService.updateByAdmin()方法不是简单 update,它包含三重防护:
@Transactional(rollbackFor = Exception.class) public boolean updateByAdmin(Long userId, Integer changeValue, String remark, Long adminId) { // 1. 时间窗口校验:同一用户24小时内最多被扣分3次,防恶意刷屏 long count = creditScoreLogMapper.countByUserIdIn24h(userId); if (count >= 3) { throw new BusinessException("该用户24小时内已被扣分3次,暂不可操作"); } // 2. 分数边界校验:最低不低于0,最高不高于100 User user = userMapper.selectByPrimaryKey(userId); int newScore = Math.max(0, Math.min(100, user.getCreditScore() + changeValue)); // 3. 日志写入 + 用户更新原子执行 CreditScoreLog log = new CreditScoreLog(); log.setUserId(userId); log.setBeforeScore(user.getCreditScore()); log.setAfterScore(newScore); log.setChangeType((byte) 2); log.setOperatorId(adminId); log.setRemark(remark); log.setCreateTime(new Date()); creditScoreLogMapper.insert(log); user.setCreditScore(newScore); userMapper.updateByPrimaryKeySelective(user); return true; }这段代码的玄学在于@Transactional注解——它保证日志写入和用户分数更新要么全成功,要么全回滚。如果只写日志不更新分数,或反之,都会导致数据不一致。答辩时被问“怎么保证一致性”,这就是标准答案。
3.3 小程序端:信用分展示不是静态值,而是实时请求 + 本地缓存双保险
首页“我的信用分”模块,pages/my/index.js中的getCreditScore()方法这样写:
getCreditScore() { const that = this; wx.request({ url: getApp().globalData.baseUrl + 'user/getCreditScore', method: 'GET', success(res) { if (res.data.code === 200) { that.setData({ creditScore: res.data.data }); // 关键:写入本地缓存,下次进入页面先显示缓存值,再拉新数据 wx.setStorageSync('creditScore', res.data.data); } }, fail() { // 失败时读取本地缓存,避免白屏 const cached = wx.getStorageSync('creditScore'); if (cached) that.setData({ creditScore: cached }); } }); }提示:
wx.setStorageSync存的是纯数字,不是对象。很多同学在这里写成wx.setStorageSync('creditScore', {score: res.data.data}),导致下次getStorageSync取出来是[object Object],页面显示 NaN。这是翻车高发区。
4. 预约冲突检测与时段管理:为什么它能撑住300人同时抢座,核心在数据库唯一索引和应用层双重锁
4.1 数据库唯一约束:reservation表的复合唯一索引是防重第一道墙
reservation表的关键字段:
CREATE TABLE `reservation` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `room_id` bigint(20) NOT NULL, `user_id` bigint(20) NOT NULL, `date` date NOT NULL, `start_time` time NOT NULL, `end_time` time NOT NULL, `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '1:已预约, 2:已使用, 3:已取消', PRIMARY KEY (`id`), UNIQUE KEY `uk_room_date_time` (`room_id`,`date`,`start_time`,`end_time`) -- 核心! ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这个uk_room_date_time索引意味着:同一房间、同一天、同一开始和结束时间,只能存在一条记录。当两个用户同时提交相同预约时,数据库会直接报Duplicate entry '101-2024-05-20-08:00:00-10:00:00' for key 'uk_room_date_time',Spring MVC 的全局异常处理器会捕获并返回“该时段已被预约”。
4.2 应用层分布式锁:ReservationService.createReservation()中的 Redis 锁是防重第二道墙
仅靠数据库唯一索引还不够——如果用户 A 和 B 同时查询“101号房2024-05-20 08:00-10:00 是否可约”,都得到“可约”,然后同时 insert,就会触发唯一索引冲突,用户体验差(看到错误提示)。所以加了 Redis 锁:
public Result createReservation(Reservation reservation) { // 构造锁key:room_101_date_20240520_start_0800_end_1000 String lockKey = String.format("room_%d_date_%s_start_%s_end_%s", reservation.getRoomId(), DateUtil.formatDate(reservation.getDate()), DateUtil.formatTime(reservation.getStartTime()), DateUtil.formatTime(reservation.getEndTime())); Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS); if (!locked) { return Result.fail("操作过于频繁,请稍后再试"); } try { // 再次查询是否已被预约(防止锁期间别人已插入) int count = reservationMapper.countByRoomDateTime( reservation.getRoomId(), reservation.getDate(), reservation.getStartTime(), reservation.getEndTime() ); if (count > 0) { return Result.fail("该时段已被预约,请选择其他时间"); } // 插入预约记录 reservation.setStatus((byte) 1); reservation.setCreateTime(new Date()); reservationMapper.insert(reservation); return Result.success(); } finally { redisTemplate.delete(lockKey); // 必须放finally,防锁永久持有 } }注意:Redis 锁的过期时间设为 30 秒,是因为最长业务耗时(查库+插库+日志)不会超过 5 秒,30 秒足够容错。
setIfAbsent是原子操作,delete放在finally是后悔药——哪怕中间抛异常,锁也会释放。
4.3 时段粒度控制:room_schedule表定义了每间房的可用时间段,不是全天开放
很多同学以为“预约 08:00-10:00”就是随便填,其实room_schedule表规定了每间房的最小预约单元:
INSERT INTO `room_schedule` VALUES (1, 101, '08:00:00', '12:00:00', 60), -- 101号房,08:00-12:00,每60分钟一节 (2, 101, '13:00:00', '18:00:00', 60), -- 13:00-18:00,同样60分钟 (3, 102, '09:00:00', '17:00:00', 30); -- 102号电脑房,09:00-17:00,每30分钟一节小程序端选择时间时,前端 JS 会根据room_id请求/room/getSchedule接口,拿到minUnit=60后,自动生成08:00,09:00,10:00... 的下拉选项,而不是让用户自由输入08:15。这是防无效预约的关键设计。
5. 避坑指南:这 4 个坑我替你踩过了,每个都让答辩前夜崩溃两小时
5.1 现象:小程序登录后首页空白,控制台报Cannot read property 'nickName' of null
原因:app.js中onLaunch里调用了wx.getUserProfile,但微信基础库版本低于 2.10.4 时不支持该 API,返回null,后续userInfo.nickName报错。
解决:打开app.js,找到wx.getUserProfile调用处,改为兼容写法:
// 替换原来的 wx.getUserProfile if (wx.getUserProfile) { wx.getUserProfile({ desc: '用于完善会员资料', success: (res) => { /* 原逻辑 */ } }); } else { // 基础库低版本走 wx.getUserInfo wx.getUserInfo({ success: (res) => { /* 原逻辑 */ } }); }5.2 现象:后台管理页“留言回复”按钮点击无反应,Network 面板显示 404
原因:tsgyy项目中MessageController.reply()接口的@RequestMapping写成了@RequestMapping(value = "/message/reply", method = RequestMethod.POST),但tsgyy-wx小程序端wx.request的 URL 拼成了http://localhost:8080/tsgyy/message/reply?id=123(把 id 当 query 参数),而 SpringMVC 默认不从 query 中取@RequestBody的 JSON 数据。
解决:修改MessageController.java,将方法签名改为接收@RequestParam:
@PostMapping("/message/reply") @ResponseBody public Result reply(@RequestParam Long id, @RequestParam String content) { // 原逻辑不变 }同时小程序端pages/message/detail.js中replyToMessage()方法里,wx.request的data改为:
data: { id: that.data.message.id, content: that.data.replyContent }5.3 现象:Navicat 导入tsgyy.sql时卡在 99%,最后报ERROR 1067 (42000): Invalid default value for 'create_time'
原因:MySQL 5.7 严格模式下,create_time datetime NOT NULL DEFAULT '0000-00-00 00:00:00'不被允许。tsgyy.sql中多张表用了该默认值。
解决:用文本编辑器打开tsgyy.sql,搜索DEFAULT '0000-00-00 00:00:00',全部替换为DEFAULT CURRENT_TIMESTAMP。共需修改 7 处(user,room,reservation,message,notice,credit_score_log,admin_log表)。
5.4 现象:Eclipse 中tsgyy项目报The project was not built since its build path is incomplete,pom.xml无红叉但src/main/java下全是红叉
原因:tsgyy是传统 Web 项目(非 Maven),但 Eclipse 误识别为 Maven 项目,导致Build Path中Maven Dependencies缺失。
解决:右键项目 →Properties→Project Facets→ 取消勾选Java和Dynamic Web Module→ 点Apply→ 再勾选回来 →Apply and Close。然后右键项目 →Configure→Convert to Maven Project(此时会生成pom.xml),再右键 →Maven→Update Project。
6. 答辩现场验证技巧:用这 3 个“可演示、可截图、可解释”的操作,10 分钟讲清技术深度
6.1 演示“信用分人工干预闭环”:从后台扣分到小程序实时刷新,全程 90 秒
这是答辩老师最爱问的点。操作路径:
- 后台登录
http://localhost:8080/tsgyy/admin/login(账号 admin/123456) - 进入【用户管理】→ 找到测试用户(如 ID=1001,信用分100)→ 点击【扣分】按钮 → 输入扣20分 → 提交
- 立刻切到小程序“我的”页面 → 下拉刷新 → 信用分变为80 → 点击“信用记录” → 查看最新一条日志:“管理员扣分 -20分”
- 关键解释:指着
credit_score_log表说,“这里change_type=2代表管理员操作,operator_id记录了操作人,before_score/after_score形成完整快照,答辩时可以导出这张表给老师看审计证据。”
6.2 演示“预约冲突防御能力”:用 Postman 模拟并发,证明双重锁生效
避免只说“我用了 Redis 锁”,要实测:
- 用 Postman 新建两个请求,URL 均为
http://localhost:8080/tsgyy/reservation/create,Body 均为:
{"roomId":101,"date":"2024-05-20","startTime":"08:00:00","endTime":"10:00:00"}- 点击 Postman 的
Runner→ 设定 2 个迭代 → 同时发送 - 观察响应:一个返回
success,另一个返回操作过于频繁,请稍后再试(Redis 锁拦截)或该时段已被预约(数据库唯一索引拦截) - 关键解释:“第一道锁是 Redis,30秒内拒绝重复请求;第二道锁是数据库唯一索引,确保最终一致性。两者结合,比单用一种更可靠。”
6.3 演示“时段粒度控制”:修改room_schedule表,立刻影响小程序可选时间
这是体现“前后端联动”的好例子:
- 在 Navicat 中执行:
UPDATE room_schedule SET min_unit=30 WHERE room_id=101; - 小程序首页 → 点击“101号房” → 进入预约页 → 时间选择器自动变成
08:00,08:30,09:00...(原来只有整点) - 关键解释:“小程序端
getSchedule接口返回min_unit,前端 JS 根据它动态生成时间选项。这样管理员改个数据库字段,就能调整所有用户的预约粒度,不用发版。”
从那以后我每次帮同学改毕设,都会先让他们跑通这三步演示——不是为了炫技,而是确保他们真理解了“信用分怎么防刷”“并发怎么防重”“配置怎么驱动前端”。这些不是 PPT 里写的“采用 Redis 缓存”,而是答辩时老师手指着屏幕问“你说的 Redis 锁在哪”,你能立刻打开ReservationService.java指给他看的那一行setIfAbsent。希望帮到你。
本文还有配套的精品资源,点击获取