计算机毕业设计的经典题目里,管理系统类永远是主力,而实验室预约系统在其中算是既有技术含量又有真实应用场景的一款。用 SSM(Spring + SpringMVC + MyBatis)做后端、微信小程序做前端,组合起来就是一个典型的"SSM 实验室预约系统小程序"。它能解决高校实验室管理中人工排班、电话预约、纸质登记带来的时间冲突和管理混乱问题,学生通过小程序就能查看空闲实验时段、在线提交预约申请,管理员在后台完成审核和发布公告。整个项目麻雀虽小五脏俱全,从数据库设计、接口开发到小程序交互、权限控制都能完整覆盖,非常适合作为计算机相关专业的毕业设计选题,也适合想系统了解"SSM + 小程序"这套组合的开发者拿来练手。下面我把这个项目从需求分析到最终落地的完整过程拆开讲一遍,包括每一步的设计思路、核心实现和踩坑记录。
1. 项目整体设计与需求拆解
1.1 实验室管理场景的痛点在哪
很多高校的实验室资源是共享的,不同专业、不同年级的课程实验、课程设计、社团活动都挤在同一批实验室里。传统管理方式基本靠"一张Excel表 + 实验室门口签到本",问题非常明显:排课信息更新不及时,学生不知道哪个时段空着;预约靠电话或者当面找管理员,效率低;临时改课、加课之后,纸质记录和实际使用经常对不上;到了期末集中实验周,实验室利用率忽高忽低,资源浪费严重。
这个题目选得好的原因就在这里——它不只是一个CRUD练习,而是真实存在管理矛盾,做出来之后能直接说明解决了什么实际问题。答辩的时候老师问"为什么做这个系统",你可以非常自然地讲出以上这几点,这比空谈"提高信息化水平"要扎实得多。
我当时在设计这个项目时,把核心问题定位在三个维度:谁能约、怎么约、如何管。"谁能约"对应角色权限,学生只能查和约,管理员负责审核和发布信息;"怎么约"对应预约流程和冲突检测,这是一个预约系统的灵魂;"如何管"对应实验室基础数据维护和使用统计。能够把这三个维度讲清楚,系统的骨架就已经立住了。
1.2 用户角色与核心需求分析
系统涉及三类用户:学生、教师、实验室管理员。每一类角色的操作路径差别很大,需求整理成表格会更清晰:
| 角色 | 核心需求 | 对应功能点 |
|---|---|---|
| 学生 | 找得到空闲实验室、快速预约、看自己的预约状态 | 实验室列表、时段查询、提交预约、取消预约、历史记录 |
| 教师 | 预约审批、查看名下预约 | 审批通过/驳回、预约列表查询 |
| 管理员 | 维护实验室信息、发布公告、统计使用率 | 实验室CRUD、公告管理、预约统计报表 |
需要注意的是,很多毕业设计做得千篇一律的问题就是把"管理员"做得太重,把学生和教师的功能做得太薄。实际上在这个项目里,学生端(小程序端)才是用户量最大、体验要求最高的部分,设计时要多花精力在小程序的页面流程和交互细节上,比如如何让用户快速看到可预约时段、提交后如何及时反馈状态。这样答辩时也能多一个讨论亮点。
1.3 功能模块划分
整个系统按模块划分大致如下:
- 用户模块:微信登录绑定学号工号、角色区分、管理员手动维护用户信息
- 实验室模块:实验室名称、位置、容量、设备说明、开放时间段、当前状态(开放/维修/关闭)
- 预约模块:按日期和时段查询空闲实验室、提交预约申请、取消预约、审核审批、预约状态流转
- 公告模块:管理员发布通知,小程序首页展示最新公告
- 统计模块:按周/月统计实验室预约次数和使用率,导出数据
模块划分不需要追求大而全,关键在于模块之间的边界清晰。比如预约模块一定要依赖实验室模块的"开放时段"数据,但不能直接操作实验室表,只能通过Service接口来调用,这样后期扩展时不会改一处崩一片。
2. 技术选型与系统架构设计
2.1 为什么是SSM + 小程序这套组合
选技术栈不能盲目跟风,要结合毕业设计的考核点和自己的基础来权衡。这套项目选型是SSM(Spring + SpringMVC + MyBatis)加微信小程序原生开发,不是没有理由的。
第一,SSM框架本身足够经典,Spring的IoC和AOP、SpringMVC的请求分发、MyBatis的半自动ORM,每一个都是面试和答辩的高频考点。相比直接上手Spring Boot,SSM更强调"配置在明处",比如SpringMVC的处理器映射、MyBatis的Mapper代理,需要你手动配置和组装,这反而逼着你把底层原理搞明白。答辩时老师一问"Spring容器是怎么管理Bean的""MyBatis的#{}和${}有什么区别",你能真正答得上来。
第二,微信小程序作为前端载体非常贴合场景。实验室预约是典型的高频低操作成本场景,学生不用安装App,扫个码或者直接搜小程序就能用。小程序原生开发上手难度比Vue/React低,页面逻辑清晰,配合微信自带的组件库,做列表、日历、表单这些界面很方便,遇到问题查文档也快。
第三,前后端交互方式简单直白。小程序通过wx.request请求后端接口,后端返回JSON,联调时只要约定好接口格式就行,不容易出现复杂的前后端纠缠。
2.2 分层架构与请求流程
后端采用经典分层架构:Controller层(接口接收)→ Service层(业务逻辑)→ Mapper层(数据持久化)。前端小程序页面对应调用不同的Controller接口。
一个典型的预约请求会这样流转:
- 小程序端在预约页点击"提交预约"按钮,通过封装好的request方法向后台发送POST请求
- SpringMVC的DispatcherServlet接收到请求,根据URL映射到对应的Controller方法
- Controller接收参数,用@RequestBody注解把JSON转成实体对象
- Service层做业务校验,检查实验室是否存在、时段是否冲突、用户是否重复预约
- Mapper层执行SQL,写入预约记录
- 返回统一的JSON结构(code、message、data)给小程序
这个流程看起来简单,但每个环节都有值得注意的细节。比如Controller接收参数时,我建议定义一个统一的Result返回类,包含code、message、data三个字段。小程序端拿到结果后先判断code是不是200,再处理data,这样不管接口成功还是失败,前端逻辑都一致。
2.3 数据库表设计思路
数据库设计是毕业设计答辩时老师很看重的部分,我强烈建议用MySQL 5.7以上版本,字符集统一utf8mb4,避免中文乱码。核心表有五张,下面逐个说明设计要点。
用户表(user):字段包括id、username、password、real_name、role、phone、wx_openid、create_time。这里的role用1表示学生、2表示教师、3表示管理员。wx_openid字段用来记录微信登录后的OpenID,这是小程序实现免密登录的关键,后面会详细讲。密码字段建议存MD5或BCrypt加密后的值,不要存明文。
实验室表(lab):字段包括id、lab_name、location、capacity、device_info、open_start、open_end、status、description。open_start和open_end是当天开放时间的起止,比如08:00到21:30。status用0和1表示关闭和开放,当实验室处于维修状态时status设为0,小程序端就不会展示可预约时段。
预约记录表(reservation):这是整个系统的核心表,字段包括id、user_id、lab_id、reserve_date、time_slot、purpose、status、create_time、audit_time。time_slot表示时段,建议用字符串形式存储,比如"08:00-10:00"。status是整个系统的核心状态:0代表待审核、1代表已通过、2代表已驳回、3代表已完成、4代表已取消。
公告表(announcement):id、title、content、create_time,管理员发布公告后小程序首页展示。
设计预约记录表时有一个关键决策:**时段用字符串直接存,还是用开始时间和结束时间两个字段?**我当时用的是time_slot字符串,因为实验室的时段是按固定档位划分的(比如每天8个时段),字符串方案直观,查询时直接等于匹配,不用做时间区间比较。如果实验室开放时间不固定、需要支持任意时间段的预约,才建议拆成begin_time和end_time两个字段。毕业设计场景下固定时段其实更贴合真实需求,也方便做冲突检测。
3. 核心功能实现与实操要点
3.1 登录认证与权限控制的落地方式
小程序端的登录认证,推荐走微信官方的能力:小程序端调用wx.login获取临时code,后端拿着code去微信接口换取openid,再把openid作为用户唯一标识。对应代码流程大致是这样的:
// 小程序端 wx.login({ success: res => { wx.request({ url: 'http://localhost:8080/api/login', method: 'POST', data: { code: res.code }, success: (resp) => { wx.setStorageSync('token', resp.data.data.token); } }) } })// 后端 @RestController @RequestMapping("/api") public class LoginController { @PostMapping("/login") public Result login(@RequestBody LoginRequest request) { String openid = getOpenidFromWx(request.getCode()); User user = userService.findByWxOpenid(openid); // 如果openid不存在,说明是第一次登录,需要绑定学号 if (user == null) { // 返回绑定提示,前端跳转到绑定页面 } String token = JwtUtil.generateToken(user); return Result.success(token); } }登录成功后,后端返回一个token(可以用JWT,也可以用自定义的UUID+Map存储),小程序端把token存到本地。之后每一个请求都在header里带上token,后端用一个拦截器统一校验token,拦截器校验不通过就返回401,前端收到401就跳转回登录页。
这里有个非常容易踩的坑:如果用户第一次微信登录时还没有绑定学号,后端就不能直接生成token,要先让前端跳转到绑定学号页面,把学号和密码提交给后端,通过校验后再返回完整token。否则就会出现用户不绑定就能访问系统的绕过漏洞,这个问题在答辩中很容易被老师追问。
权限控制方面,我用了一个相对简单的方案:在SpringMVC拦截器中判断请求路径前缀。/api/admin/**路径只允许管理员角色的token访问,/api/teacher/**允许教师和管理员访问,/api/student/**允许所有登录用户访问。接口层面再用@RequireRole这样的自定义注解做细粒度控制,这样写起来干净,也方便讲述设计思路。
3.2 核心预约流程的实现与冲突处理
预约流程是整个项目的核心,我把完整状态流转画在脑子里,然后才动手写代码:学生提交预约 → 状态为待审核 → 管理员/教师审核通过或驳回 → 通过后自动变为已通过,使用时间结束后修改为已完成;如果学生临时有事,也可以在待审核或已通过状态下取消预约。
关键点是怎么防止同一实验室同一时段被重复预约。最直接的做法是在Service层做两步操作:先查询reservation表中是否存在同一lab_id、reserve_date、time_slot且status不等于已取消的记录,如果存在就返回"该时段已被占用";不存在才插入预约记录。但这里有个隐患——高并发场景下两个请求同时查询都发现没有记录,然后同时插入,就会产生冲突。
毕业设计阶段不需要引入分布式锁这种重型方案,我用的是MySQL的唯一索引加数据库事务:
ALTER TABLE reservation ADD UNIQUE KEY uk_lab_date_slot_status (lab_id, reserve_date, time_slot, status);不过这个方案有个限制:status不同时,同一条时段记录会有多条(一条待审核、一条已完成),所以唯一索引不能覆盖所有情况。实际操作中更稳妥的方案是:把"已占用"判断单独拆出来,在插入前先加锁查询(SELECT ... FOR UPDATE),或者引入一个单独的预约时间表(reservation_slot)用来做唯一约束。预约时间表只存lab_id、reserve_date、time_slot三个字段,加唯一索引,插入成功则代表该时段被锁定,预约记录表再保存详细关联信息。这种方式在并发测试下基本不会出问题,实现成本也不高。
提交预约的Service层核心流程,可以这样写:
@Transactional public Result createReservation(ReservationDTO dto) { // 1. 校验实验室是否存在且处于开放状态 Lab lab = labMapper.selectById(dto.getLabId()); if (lab == null || lab.getStatus() != 1) { return Result.error("实验室不可预约"); } // 2. 校验时段是否在开放时间范围内 if (!checkInOpenTime(lab, dto.getTimeSlot())) { return Result.error("该时段不在实验室开放时间范围内"); } // 3. 尝试插入预约时段表,捕获唯一索引冲突异常 try { reservationSlotMapper.insert(dto.getLabId(), dto.getReserveDate(), dto.getTimeSlot()); } catch (DuplicateKeyException e) { return Result.error("该时段已被预约,请换一个时段"); } // 4. 插入预约主记录 Reservation reservation = new Reservation(); // ... 字段填充 reservationMapper.insert(reservation); return Result.success("预约申请提交成功,请等待审核"); }这里有个细节值得提一下:取消预约时,一定要先把reservation表记录的status改为已取消,再把reservation_slot表中对应的记录删除,因为冲突检测依赖的是reservation_slot的锁定记录。如果只改主表状态而不删除slot记录,那个时段就会一直处于不可预约状态,用户会以为系统出bug了。这个bug我在开发时就踩过,后来在取消预约接口里补上了对应删除逻辑才解决。
3.3 小程序端页面与交互实现
小程序端我规划的页面包括首页、实验室列表页、预约页、我的预约页、个人信息页。因为页面不算多,用原生小程序写就够,没有引入uni-app等跨端框架。
几个值得记录的实现细节:
列表加载更多是很多毕设容易忽略的点。实验室列表和我的预约记录都是典型的列表场景,我用了小程序的onReachBottom监听页面滚动到底部,然后翻页加载。后端接口在设计时就支持分页参数,比如currentPage和pageSize,Mapper层用PageHelper或者手写LIMIT完成分页。前端定义一个data对象,里面有list和pageNum两个字段,每次加载更多时pageNum加1,把新数据concat到list中,同时判断返回数据是否小于pageSize来决定是否还有下一页。
页面顶部标题的动态设置,触达的知识点很实用。如果实验室名称是动态的,小程序可以通过wx.setNavigationBarTitle({ title: 'xxx实验室' })来修改当前页面标题,这样预约页打开时就能直接显示"正在预约的实验室名称",体验会好很多。
请求封装这个事一定要做。小程序原生的wx.request用起来有重复代码,而且很容易回调地狱。我封了一个简单的request方法:
function request(url, method, data) { return new Promise((resolve, reject) => { wx.request({ url: API_BASE_URL + url, method: method, data: data, header: { 'Content-Type': 'application/json', 'token': wx.getStorageSync('token') }, success: (res) => { if (res.data.code === 200) { resolve(res.data.data); } else if (res.data.code === 401) { wx.removeStorageSync('token'); wx.navigateTo({ url: '/pages/login/login' }); reject(new Error('登录过期')); } else { wx.showToast({ title: res.data.message, icon: 'none' }); reject(new Error(res.data.message)); } }, fail: (err) => { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }) }) }封装完之后,页面里调接口就是非常清爽的写法,例如获取我的预约记录:
async getMyReservations() { const res = await request('/api/reservation/my', 'GET', {}); this.setData({ list: res.records }); }时间段选择组件是预约页里交互最重的部分。我用了横向可滚动的时段列表,每个时段是一个按钮,按钮颜色区分状态:灰色代表已被预约、绿色代表可预约、蓝色代表当前选中。用户点选时段后再填用途描述,然后提交。因为后端已经做了冲突检测,前端也需要做同样的灰化处理来提升体验,唯一的办法是接口返回该实验室当天的空闲时段列表,前端直接渲染空闲时段,满了的时段根本不展示。
3.4 后端的实验室管理与统计实现
管理端功能做在同一个后台项目里,用网页端管理。和普通CRUD的区别在于,实验室的开闭状态会直接影响小程序端的可预约性,所以管理端修改实验室信息时,要考虑到已存在的预约记录。例如把实验室从"开放"改为"关闭"时,需要提示管理员"该操作不会影响已通过的预约记录",避免产生数据不一致。更严谨的做法是在改状态时,把待审核状态的预约自动驳回。
统计模块其实是答辩加分项。我实现了两个指标:一是每个实验室每月预约次数排行,二是实验室使用率(已预约时段数/总开放时段数)。SQL写法可以直接聚合:
SELECT lab_id, COUNT(*) AS reserve_count FROM reservation WHERE reserve_date BETWEEN #{startDate} AND #{endDate} AND status IN (1, 3) GROUP BY lab_id ORDER BY reserve_count DESC按周统计使用率时,先算出每个实验室每周的总开放时段数,再统计实际预约数,两者相除就是使用率。统计结果直接返回给管理端页面用表格展示,不做图表也行,如果时间充裕,前端用ECharts画个柱状图会很加分。
4. 常见问题排查与避坑心得
4.1 接口联调阶段的经典问题
第一个问题就是跨域。如果你的小程序页面在微信开发者工具里请求本地后端接口,域名是http://localhost:8080,需要在开发者工具的"详情"设置里勾选"不校验合法域名"。但注意,这只是开发环境可用,真机预览或者上传体验版时,必须把后端接口地址改成已备案的HTTPS域名。小程序正式环境要求所有请求域名必须HTTPS且已在微信公众平台配置,这点不提前准备,后期联调会被卡住。
第二个问题是中文乱码。这个项目从数据库到前后端都要保证UTF-8编码。MySQL连接串里加上characterEncoding=utf8,SpringMVC的RequestMappingHandlerAdapter里配置UTF-8消息转换器,Tomcat的server.xml中URIEncoding设为UTF-8。任何一个环节漏掉,小程序的请求数据到了后端都会变成乱码。
第三个问题是时间类型的序列化格式。后端通过Jackson返回LocalDateTime时,默认序列化格式是2024-01-15T10:30:00这种带T的格式,小程序端处理起来很麻烦。我建议在applicationContext配置里统一设置时间格式:
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss或者在Java类上用@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")注解,这样前端拿到的时间就能直接展示,省去格式化逻辑。
4.2 预约冲突的并发隐患
这个问题前面已经有方案,但这里再补一个我在并发测试时发现的细节。用JMeter模拟20个并发请求同时预约同一实验室同一时段时,如果没有锁和唯一索引,会插入多条重复记录。加了唯一索引和事务后,正确率是100%,但接口的响应时间会略有上升,因为并发请求都在等锁。
在实际操作中有个优化技巧:不必让所有请求都在事务里等待锁,可以在插入前先做一次直接查询(普通SELECT),如果发现已经有记录就直接返回失败,这是"快速失败"路径。只有查询没发现记录时才走插入逻辑,插入时再靠唯一索引兜底。这种方式可以大幅减少无效的事务等待,我在压测中响应时间从平均1200ms降到了200ms以下,效果很明显。
4.3 小程序发布的那个坑
当整个项目开发完毕,要上传体验版时,还有几个容易被忽略的步骤。小程序上传代码前需要确认AppID已经设置正确;上传后在微信公众平台配置request合法域名,域名必须是HTTPS且经过备案;发布正式版之前还需要进行小程序年审,这些流程如果提前不熟悉,会临时手忙脚乱。
还有一个很多人会忽略的问题是底部tabBar的图标。小程序原生tabBar默认需要图标,但是很多毕设项目只做2到3个tab页面,其实可以不使用tabBar,直接用一个首页按钮跳转来替代,省去准备图标的麻烦。不过如果做tabBar,图标可以不上传吗?不行,tabBar每个item都至少需要一个图标,否则编译会报错。
4.4 源码的使用方式建议
项目标题里写了"赠源码",拿到源码之后千万不要直接改个名字就交上去。我见过太多的雷同毕设,连数据库脚本都是原封不动的,老师一眼就能看出来。正确做法是把源码当作参考和学习脚手架:
第一,先完整跑起来,把数据库脚本导入MySQL,修改数据库连接配置,启动后端,用开发者工具打开小程序,把整条预约流程走一遍。第二,理解每个模块的代码结构,重点搞懂Service层的业务逻辑和Mapper层的SQL,然后试着把某一块自己重写一遍,比如把预约冲突检测从唯一索引方案改成基于状态字段加事务的方案。第三,在原有功能上增加一个自己的创新点,比如增加一个实验室设备借用功能、添加一个自由时段的日历控件,都能让项目差异化明显提升。
答辩的时候,老师最看重的是你是不是真的理解这个项目。如果你能解释清楚为什么用SSM而不是单独用SpringBoot、为什么预约冲突检测用唯一索引而不是纯代码判断、微信登录的openid在整个系统中扮演什么角色,盲审和答辩都不会为难你。
5. 一些经验之谈
这个项目做下来,我个人最大的体会是:毕业设计的坑大多不是因为某个技术有多难,而是因为把精力用错了方向。实验室预约系统核心难点就两个,一个是预约冲突检测,一个是微信登录,其他全是标准CRUD。把这两个难点想清楚、写明白,项目就成功了一大半。剩下的时间应该花在打磨演示流程和准备常见答辩问题上,而不是反复纠结某个页面样式。
最后再分享一个小技巧,也是我在项目中用得比较多的:在后端统一使用日志记录每个关键接口的入参和耗时。用Spring AOP写一个切面,在Controller方法执行前后打印日志。这样开发调试、答辩演示时排错都非常快,老师问"你用什么手段定位问题",你也有一个很落地的回答可以讲。这个系统本身规模不大,但如果你能把它每一个模块的技术细节都吃透,它完全可以成为你简历上一个拿得出手的实战项目。