☰
SSM+微信小程序实验室预约系统设计与实现全指南
2026/10/2 2:58:16 网站建设 项目流程

1. 先把题目拆开:这个系统到底要做什么

每年毕设开题,实验室预约系统这个题目都会准时出现。它全称通常是“SSM实验室预约系统小程序设计与实现”,后面可能还跟着一串数字,比如 75652 这种,不用太当真,多半是学校选题管理系统的入库编号。真正的核心就两件事:后端用 SSM 写一套实验室预约管理接口,前端用微信小程序做预约入口。把这两条线打通,就是一套标准的本科毕业设计项目。

这类项目我做过不少,也带过学弟学妹跑通过。先说结论:这个题目很适合作为全栈练手,但不适合只对着源码闷头改。你需要的不是把源码背下来,而是能讲清楚为什么要这样设计表、为什么同一个时段不能两个人预约、为什么小程序页面要分页加载。这篇文章我就按我平时带项目的思路,从需求拆解、技术选型、数据库设计、后端代码、小程序端实现,一直讲到源码包怎么消化和答辩怎么回答。

1.1 角色与核心流程

一套完整的实验室预约系统,按最常见的角色设计,至少要包含三类人:

  • 学生:登录小程序,查看实验室列表、查看实验室详情和设备信息,选择日期和时段提交预约,查看自己的预约记录,取消未开始的预约。
  • 管理员:维护实验室基本信息,审核预约,查看所有预约记录,发布公告。
  • 教师:有的题目会把教师单独作为一个角色,用来审批预约;如果题目没强制要求,完全可以把审批功能合并到管理员,别给自己加工作量。

核心流程也非常清晰:学生提交预约 → 系统检查该时段是否冲突 → 生成待审核记录 → 管理员审核 → 学生看到通过结果 → 在规定时间到实验室 → 预约状态变为已完成。

这里我建议你把“预约”理解成一个资源占用过程,而不是简单的数据插入。实验室一个时段只能被一个预约占用,所以系统必须有一个机制来保证“同一时段只有一个人能约上”。这是整个项目的核心难点,后面我会专门展开讲。

1.2 功能边界:先做减法

很多学生拿到源码以后,第一反应是“功能太少了,我能不能加个论坛、加个消息推送、加个在线聊天”。我的建议是不要。毕设评分看的不是功能数量,而是完整度和逻辑严谨性。

一个合适的范围大概是:

  • 实验室管理:增删改查、开放/关闭状态、容量、设备说明。
  • 预约管理:学生提交预约,管理员审核,支持取消、驳回、备注。
  • 用户体系:微信登录或账号密码登录,区分学生和管理员。
  • 公告管理:管理员发布一些简单的通知。
  • 个人中心:查看我的预约、退出登录。

不该做的就别做,比如支付、消息队列、物联网硬件联动。题目叫“实验室预约系统”,核心就是预约两个字。功能堆得太多,答辩时每一个点都可能被老师追问,反而容易露怯。

2. 技术选型逻辑:为什么选 SSM,以及它和 SpringBoot 的真实差距

说实话,放到现在的生产环境,新项目再用 SpringMVC + Spring + MyBatis 这套老组合已经不多见了,很多人会直接上 SpringBoot。但毕设题目既然写了 SSM,就按 SSM 做。理由很现实:课程里讲的是 SSM,答辩老师熟悉的也是 SSM,你换了框架反而容易被问“为什么不用题目要求的 SSM”。

2.1 SSM 三个框架各自的分工

SSM 不是三个并列的东西,而是三个层次分明的组件:

  • Spring:核心容器。它负责管理 Service、Mapper 这些对象的创建和依赖注入,所以代码里不需要到处 new 对象,用注解声明就行。
  • SpringMVC:负责 HTTP 层。浏览器的请求通过 DispatcherServlet 分发到 Controller,Controller 处理完再返回 JSON 或页面。
  • MyBatis:负责数据库访问。你把 SQL 写在 Mapper 接口或者 XML 里,它负责把查询结果映射成 Java 对象。

它们的关系可以简单理解成:SpringMVC 接收请求,Spring 管理中间的业务对象和事务,MyBatis 操作数据库。三层各管各的,这就是为什么代码分层也要对应着来。

2.2 为什么小程序端适合这种题

微信小程序在毕设里特别常见,因为不需要上架应用商店,微信开发者工具里直接就能跑,提交作业也方便。而且小程序本身就是前后端分离的客户端,天然要求后端提供 JSON 接口,这对训练接口设计能力很有帮助。

后端只需要把接口写好,返回 JSON,小程序用 wx.request 调用。你不需要写复杂的前端页面,WXML 语法比 HTML 简单得多,样式也比传统 CSS 更容易上手。一个普通学生从零开始,两三周就能把页面结构搭完。

2.3 项目结构约定

无论源码包怎么给你,后端代码建议保持这样的包结构:

com.example.lab ├── controller # Controller 层,接收 HTTP 请求 ├── service # Service 接口 │ └── impl # Service 实现类 ├── mapper # MyBatis Mapper 接口 ├── entity # 数据库实体类 ├── dto # 接收前端参数的对象 ├── vo # 返回给前端展示的对象 └── config # 配置类

很多人写毕设喜欢把逻辑全堆在 Controller 里,一个方法干所有事。这样代码能跑,但答辩时老师一看就知道分层意识不足。Controller 里只做参数接收和结果封装,业务判断放到 Service,SQL 放到 Mapper,三层各司其职。

3. 数据库设计:预约不冲突的关键全在表结构

我第一次做类似项目时,把预约表设计得很简单:一个 user_id、一个 lab_id、一个 start_time、一个 end_time,结果查冲突特别痛苦。后来我才想明白,预约系统不应该用“时间段”字段做自由匹配,而应该先把一天切成固定时间片,谁选了这个时间片,这个时间片就被占用。这样设计表结构,后面的冲突判断会简单很多。

3.1 核心表与字段

以最常见的需求为例,核心表大概有这几张:

表名核心字段说明
userid, username, password, real_name, role, phone, openid, create_timerole 用 0 学生 1 管理员 2 教师
labid, lab_name, location, capacity, equipment, status, create_timestatus 0 关闭 1 开放
time_slotid, start_time, end_time, sort_no固定时间片
lab_slotid, lab_id, slot_date, time_slot_id, status某实验室某天某个时间片的状态
reservationid, reservation_no, user_id, lab_id, lab_slot_id, slot_date, time_slot_id, purpose, status, audit_user, audit_time, create_time预约记录
announcementid, title, content, create_time公告

你可能会问,为什么要有 lab_slot 这张表,直接在 reservation 表里判断冲突不行吗?当然可以,但判断语句要处理时间区间重叠,逻辑比较绕。而 lab_slot 相当于把可用的实验室时段提前生成好,学生预约时就是“占一个已经存在的坑位”,没坑位就是冲突。

3.2 时间模型:自由时段 vs 固定时间片

自由时段的意思是学生可以自己选“8:30–9:20”这种任意区间。这种交互看起来灵活,但实现起来要处理两个区间是否重叠,而且容易出现脏数据。固定时间片的做法则是把一天切成几段,比如:

  • 08:00–09:40
  • 10:00–11:40
  • 14:00–15:40
  • 16:00–17:40

学生只能从这些时间片里选一个。这样设计有几个好处:

第一,页面展示简单,前端只需要把“可用/不可用”的格子画出来。第二,后端冲突判断简单,查 lab_slot 表里那条记录的状态即可。第三,数据库层面可以加唯一索引,防止同一天同一实验室同一个时间片被重复生成。

你甚至可以一段代码生成一周的 lab_slot 数据:

CREATE TABLE lab_slot ( id INT PRIMARY KEY AUTO_INCREMENT, lab_id INT NOT NULL, slot_date DATE NOT NULL, time_slot_id INT NOT NULL, status TINYINT DEFAULT 0 COMMENT '0 可用 1 已占用', UNIQUE KEY uk_lab_date_slot (lab_id, slot_date, time_slot_id) );

3.3 状态字段与审核信息

预约表里最重要的字段是 status,我推荐用整数表示,不要直接存中文。一套常见的状态设计如下:

值含义说明
0待审核学生提交成功后默认状态
1已通过管理员审核通过
2已驳回管理员审核不通过
3已取消学生取消预约
4已完成预约时间结束后变为完成

除了 status,还要有 audit_user 和 audit_time,记录谁审核的、什么时候审核的。很多源码里只放状态不放审核人,后面写“管理员操作记录”的时候就没法查。

4. 后端实现:Controller、Service、Mapper 三层的代码习惯

后端是整个系统的核心。源码包里通常已经写好了完整代码,但你不能只是跑通,得知道每个注解是干什么的,Controller 里为什么这样写,MyBatis 的 SQL 为什么要用 #{}。

4.1 SSM 常用注解一表看懂

你可以把下面这些注解当作基础清单,答辩时大概率会被问到:

注解作用
@Controller声明这是一个 SpringMVC 控制器
@ResponseBody把方法返回值序列化成 JSON 写入响应体
@RestController@Controller + @ResponseBody 的组合
@RequestMapping映射 URL 和 HTTP 方法
@RequestParam绑定单个请求参数
@PathVariable绑定 URL 路径变量
@RequestBody把前端传的 JSON 绑定到对象
@Service声明 Service 层组件
@Repository声明 Mapper 层组件
@Autowired自动注入依赖对象
@Transactional声明事务,通常加在 Service 方法上
@Param给 Mapper 接口参数命名

很多老项目习惯用 @Controller + @ResponseBody 组合,因为课程里一般这样教。如果你用 @RestController 也行,Spring 4 之后就支持。答辩时能说清两者关系就行。

4.2 Controller 层示例

Controller 不要写太多业务。它应该只做三件事:接收参数、调用 Service、把结果返回给前端。

@Controller @RequestMapping("/api/reservation") public class ReservationController { @Autowired private ReservationService reservationService; @RequestMapping(value = "/add", method = RequestMethod.POST) @ResponseBody public Result add(@RequestBody ReservationDTO dto, HttpSession session) { User loginUser = (User) session.getAttribute("loginUser"); if (loginUser == null) { return Result.error("请先登录"); } dto.setUserId(loginUser.getId()); return reservationService.createReservation(dto); } }

请注意,这里我把 userId 从 session 里取出来,而不是让前端传。这个细节很关键,否则学生可以伪造 userId 帮别人预约,属于越权漏洞。

DTO 也不要偷懒使用 Map 接收参数。DTO 能明确字段,后端可以校验必填项,前端一看就知道要传什么。

4.3 Service 层事务与 MyBatis 动态 SQL

业务判断必须放在 Service。比如创建预约时,要先检查 lab_slot 状态,再占用资源,再插入预约记录。这三个步骤必须在一个事务里,否则可能出现“占用成功但预约记录没插上”的数据不一致。

@Service public class ReservationServiceImpl implements ReservationService { @Autowired private LabSlotMapper labSlotMapper; @Autowired private ReservationMapper reservationMapper; @Transactional @Override public Result createReservation(ReservationDTO dto) { LabSlot slot = labSlotMapper.selectForUpdate(dto.getLabSlotId()); if (slot == null || slot.getStatus() == 1) { return Result.error("该时段已被预约"); } labSlotMapper.updateStatus(dto.getLabSlotId(), 1); Reservation reservation = new Reservation(); reservation.setReservationNo(generateNo()); reservation.setUserId(dto.getUserId()); reservation.setLabId(dto.getLabId()); reservation.setLabSlotId(dto.getLabSlotId()); reservation.setStatus(0); reservationMapper.insert(reservation); return Result.success(); } }

selectForUpdate 的意思是查询时加行锁,两个学生同时预约同一个 lab_slot 时,第二个事务会等第一个事务提交后才查到最新状态,从而避免超卖。这个方法在毕设答辩里算是加分项。

MyBatis 里写动态查询时,推荐用 XML 里的 where + if:

<select id="selectByCondition" resultType="com.example.lab.entity.Reservation"> select * from reservation <where> <if test="userId != null"> and user_id = #{userId} </if> <if test="status != null"> and status = #{status} </if> <if test="labId != null"> and lab_id = #{labId} </if> </where> order by create_time desc </select>

这里必须强调一点:参数占位符永远用 #{},不要用 ${}。因为 #{} 是预编译处理,能防止 SQL 注入;${} 是字符串拼接,用户输入可能变成 SQL 的一部分。

5. 小程序端实现与调试:从零到真机预览

小程序端看起来是纯页面的活,但实际写起来也一样有讲究。尤其是列表加载更多、动态标题、接口调试,这些关键词在网上一搜全是问题,其实都是基础功。

5.1 页面结构与路由设计

小程序端建议按功能分目录,不要所有页面都平铺在 pages 下面。一个合理的结构是:

pages/ ├── login/ # 登录页 ├── home/ # 首页 ├── lab/ │ ├── labList # 实验室列表 │ └── labDetail # 实验室详情 ├── reservation/ │ └── reserve # 提交预约页 ├── mine/ │ ├── mine # 个人中心 │ └── myReservation # 我的预约记录 └── admin/ └── auditList # 管理员审核列表

在 app.json 里可以设置全局导航栏标题:

{ "pages": [ "pages/login/login", "pages/home/home" ], "window": { "navigationBarTitleText": "实验室预约系统", "navigationBarBackgroundColor": "#4A90D9", "navigationBarTextStyle": "white" } }

5.2 列表加载更多:onReachBottom 的正确姿势

“小程序页面列表加载更多”是特别常见的需求。很多人一开始是写一个“加载更多”按钮,但更自然的交互是页面滚动到底部自动加载。微信小程序提供了 onReachBottom 生命周期函数,触发后请求下一页数据。

直接贴一段可用的实现:

Page({ data: { page: 1, pageSize: 10, list: [], isLastPage: false, isLoading: false }, onLoad() { this.loadList(); }, onReachBottom() { if (this.data.isLastPage || this.data.isLoading) { return; } this.setData({ page: this.data.page + 1 }); this.loadList(); }, loadList() { this.setData({ isLoading: true }); wx.request({ url: 'http://127.0.0.1:8080/lab/api/reservation/my', data: { page: this.data.page, pageSize: this.data.pageSize }, success: (res) => { const records = res.data.data.records || []; this.setData({ list: this.data.list.concat(records), isLastPage: records.length < this.data.pageSize }); }, complete: () => { this.setData({ isLoading: false }); } }); } });

注意两点。第一,onReachBottom 里要先判断 isLastPage 和 isLoading,否则会重复请求。第二,success 回调里要用箭头函数,否则 this 指向会出错。

如果还做了下拉刷新,需要在 onPullDownRefresh 里把 page 重置为 1,把 list 清空,再重新加载。

5.3 动态标题和离开页面时的处理

小程序头部标题默认在 app.json 里写死,但有些页面需要动态改。比如实验室详情页,要根据不同实验室显示不同标题。小程序提供了 wx.setNavigationBarTitle 接口:

wx.setNavigationBarTitle({ title: '计算机实验室 A301' });

再比如“微信小程序如何监听用户离开小程序”这类问题,其实就是 onHide 和 onShow 的使用场景。当用户切到后台再回来时,我的预约列表很可能已经发生变化,所以我会在 onShow 里重新拉一次数据,避免用户看到旧状态。

这不算复杂技术,但很多新手想不到,写进论文里也是一个体验细节。

5.4 用 Charles 抓包排查接口问题

后端接口写完,小程序连不上,是日常最容易遇到的问题。接口 404、500、参数对不上,只看控制台不够直观,我一般会用 Charles 抓包看请求。

开发阶段的步骤很简单:电脑和手机连同一个局域网,在微信开发者工具里勾选“不校验合法域名”来连接本地后端。真机调试时,把 Charles 的监听端口配置到手机 WiFi 的网络设置里,然后就能看到小程序发出的请求和响应。

抓到包以后重点看三样东西:请求 URL 是否正确、请求参数是否和后端 DTO 对应、响应 JSON 是否正常。我遇到最多的类型是 baseUrl 没改,后端在 8080,小程序却请求了 80 端口。

6. 预约状态机与并发:最容易丢分也最能讲出亮点的地方

实验室预约系统最核心的难点不是增删改查,而是“同一个实验室、同一天、同一个时间片,只能有一个预约”。如果这个问题答不好,分数会受影响;答好了,反而能成为整个答辩的亮点。

6.1 状态流转:不要把预约做成一条死流程

预约状态不能只设计成“预约成功”和“预约失败”,因为真实业务存在审核、驳回、取消。

我推荐的状态设计是:

状态值状态名触发动作
0待审核学生提交
1已通过管理员审核通过
2已驳回管理员审核不通过
3已取消学生取消
4已完成时间过后由系统更新

在代码里不要散落魔法数字,可以定义常量类:

public interface ReservationStatus { int PENDING = 0; int APPROVED = 1; int REJECTED = 2; int CANCELED = 3; int FINISHED = 4; }

取消预约时要校验状态,只有待审核和已通过可以取消,已驳回和已完成不能取消。这就是一个简单的状态机约束。

6.2 并发冲突的两种解法

先说不推荐的解法:先查一遍该时段有没有预约,没有就插入。这种“先查后插”在高并发下会出问题,两个学生同时操作,两个事务都查到“没有预约”,然后都插入成功,就出现了超卖。

解决办法有很多种,我建议毕设用 lab_slot 加行锁的方式。核心是 lab_slot 表里每个时间片只有一条记录,预约时用 select ... for update 锁定这条记录:

SELECT id FROM lab_slot WHERE id = #{labSlotId} AND status = 0 FOR UPDATE;

如果查不到记录,说明该时段已经被占用,直接返回“该时段已被预约”。如果能查到,占用这个 lab_slot,再插入预约记录。因为行锁的存在,第二个并发请求会等待第一个事务提交后,再执行查询,此时 status 已经是 1,自然就失败。

这种设计的思维是:先锁定资源,再创建业务记录。比单纯的查询判断要可靠得多。

6.3 前端后端的双重校验

很多学生在做预约页时,只在点击按钮后提示一下“预约成功”,后端再没校验。这不行。

前端校验的目的是提升用户体验,比如日期不能是过去日期、时间片不能重复选择、没有选择实验室就不能点提交。后端校验的目的是保证数据正确,比如用户是否登录、lab_slot 是否存在、状态是否是可用、用户是否已经预约过同一个时段。

记住一句话:前端校验可以没有,后端校验必须有。接口不能信任任何人传进来的参数。

7. 源码包拿到手后:跑通、改造、答辩全流程建议

题目里写了“赠源码”,拿到源码包之后,大多数人的第一反应是解压、导入、跑通。这个流程没错,但跑通只是开始。源码包的价值在于参考,不在于直接提交。

7.1 本地部署三步走

第一步,在 MySQL 里新建数据库,导入源码自带的 sql 脚本。注意检查数据库字符集,建议用 utf8mb4,否则中文容易乱码。

第二步,修改数据库连接配置。典型文件是 jdbc.properties 或者 application.properties:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/lab_reservation?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456

第三步,用 IDEA 把后端跑在 Tomcat 上,然后在微信开发者工具里导入小程序项目,修改 baseUrl 指向本地后端。如果一切正常,就能看到小程序登录页。

想发给同学试用收集反馈,直接用微信开发者工具的“预览”功能生成二维码,同学微信扫码就可以打开体验版。不过真机访问本地后端会比较麻烦,要么把后端接口部署到公网服务器,要么使用内网穿透工具,这个看你自己手头有什么资源。

7.2 二次开发清单:把别人的源码变成你的

源码包再完整,也是别人的思路。我建议你在跑通后做至少两处二次开发,不要直接交原版。

比较实用的改造方向:

  • 增加公告管理模块,管理员能发布公告,首页展示公告列表。
  • 给“我的预约”增加状态筛选标签,比如待审核、已通过、已完成。
  • 在预约表里增加取消原因字段,完善状态流转。
  • 给管理员增加一个简单的统计页面,统计每天各实验室使用率。

每做一个改动,就把对应部分写进自己的论文“系统实现”章节。答辩老师看到的不再是一个陌生源码,而是你能自己改动的项目。

7.3 答辩时一定会被追着问的几个问题

最后整理几个我见过的高频答辩问题,每个都能在这篇文章里找到答案:

  • 为什么用 SSM 不用 SpringBoot?因为课程体系和题目要求,SSM 分层清晰,SpringMVC 负责请求分发,Spring 负责对象管理和事务,MyBatis 负责 SQL 映射。
  • 同一时间段两个人同时预约怎么办?lab_slot 表加行锁,select for update 锁住资源,第二个事务看到状态已占用,预约失败。
  • SQL 注入怎么防范?MyBatis 使用 #{} 预编译参数,不用 ${} 拼接用户输入。
  • 小程序怎么和后端通信?wx.request 发 HTTP 请求,后端返回 JSON,小程序解析后渲染页面。
  • 用户没登录能不能调用预约接口?不能,用户信息从 session 中获取,拦截器统一校验登录状态。

这些问题是这类的标配。你不用背标准答案,只要真把项目跑通并自己改过,都能答得上来。

最后说一点我自己的经验。源码包是帮你节省时间的,不是帮你跳过思考的。拿到项目后,第一件事不是急着看代码,而是先把数据库脚本从头读一遍,看看每个表、每个字段为什么存在。尤其是 lab_slot 的设计逻辑、预约状态流转、并发冲突处理,这三个点你闭上眼睛都能画出来时,这个项目才算真正变成你自己的。后面哪怕再收到类似的预约系统题目,换个实验室、换个场地,你也能很快改出一套新系统。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询