每年到这个时候,总有不少同学在毕设选题上犯难。如果是计算机相关专业,想找一类“工作量够、技术栈完整、演示效果好、答辩好讲”的题目,基于Spring Boot + Vue的健身房预约小程序系统属于一个非常经典的组合。它不像纯管理系统那样单薄,也不像算法类题目那样容易卡在理论上。小程序端、后台管理端、服务端三层都涉及,数据库设计也有足够的细节可挖,整套做下来既能体现工程能力,又能讲清楚业务逻辑。
这篇内容我按“从选题到落地”的顺序完整拆一遍。包括功能模块怎么划、数据库表怎么设计、预约类系统最关键的时间冲突和并发扣减怎么处理、小程序和Vue后台分别有什么要注意的坑,以及最后答辩和文档准备的一些经验。适合正在做类似项目的学生,也适合自己接私活想快速搭一套预约类小程序的人参考。
1. 选题价值与整体设计思路
1.1 为什么选“健身房预约小程序”当毕设
毕设选题有一个很现实的原则:既要能做出东西,又要有得写、有得讲。健身房预约小程序恰好满足这几点。
第一,需求足够明确。去健身房的人要查看课程、预约私教课或者团课、查看自己的预约记录、取消预约;健身房管理员要维护课程、排课、处理预约、统计上课人数。这些需求任何一个去过健身房的人都能理解,需求分析章节不需要胡编乱造。
第二,技术栈覆盖面好。Spring Boot 负责后端接口,Vue 负责后台管理页面,小程序负责 C 端用户操作,MySQL 负责数据存储。一个项目把前后端分离、小程序开发、数据库设计、接口安全(登录态、鉴权)全都覆盖到了,工作量和难度刚好在一个本科生能独立完成的范围里。
第三,演示效果好。小程序在手机上的交互非常直观,答辩现场直接把小程序跑起来,用户从浏览课程到提交预约再到查看记录,整个过程几分钟就能走完,比单纯打开网页演示更有说服力。
我个人在实际带过几届类似题目后有一个感受:这类预约系统的核心难点不在“增删改查”,而在“预约”这个动作背后的一连串逻辑。怎么防止重复预约、怎么处理人数上限、怎么管理取消和爽约、怎么避免超卖,这些才是设计文档里能体现水平的地方,也是数据库设计要重点体现的内容。
1.2 系统角色与功能模块拆解
我把整个系统按角色拆成三端:普通用户(小程序端)、健身房管理员(后台管理端)、系统游客(未登录用户)。
-
用户端(微信小程序)
- 微信授权登录,绑定手机号
- 浏览健身房公告、课程列表、教练介绍
- 查看某一天或某一时段的排课信息
- 选择排课进行预约、取消预约
- 查看“我的预约”列表,区分待上课、已完成、已取消、已爽约
- 个人中心:修改昵称头像、查看历史记录
-
管理端(Vue 后台)
- 管理员登录(账号密码,配合 JWT 鉴权)
- 课程管理:新增、编辑、上下架课程
- 教练管理:维护教练信息、绑定授课课程
- 排课管理:按课程 + 教练 + 时间生成排课记录,设置人数上限
- 预约管理:查看所有用户预约,支持手动取消异常预约
- 数据统计:按日/按月统计预约量、课程热度
-
公共服务能力
- 定时任务:上课时间过后自动把预约状态改为“已完成”或“爽约”
- 公告管理:管理员发布、用户端滚动展示
这里有一个容易被忽略但很重要的事:会员卡、支付、余额这类功能我在 V1.0 版本里不推荐做。原因很简单,接入微信支付需要企业资质,个人开发者在毕设阶段很难搞定。即便你做了,也大概率只能写在文档里、演示时不能真正跑通。毕业设计追求的是“核心流程闭环”,预约闭环已经足够支撑工作量,支付这类模块等文档工作量不够或者想做得更完整时再说。
1.3 技术栈选型:不是越新越好
很多同学一上来喜欢追新版本,Spring Boot 3.x、Vue 3.5、JDK 21……但毕设项目有个特殊性:它需要的是稳定性和可控性。你选一个我都很熟但网上资料相对少、第三方组件还没完全跟进的新版本,调试成本会非常高。
我建议的组合:
| 层次 | 技术选型 | 说明 |
|---|---|---|
| 后端框架 | Spring Boot 2.7.x | 稳定、资料多,与 MyBatis-Plus、Druid 兼容性好 |
| 持久层 | MyBatis-Plus | 大幅减少单表 CRUD 代码,分页插件好用 |
| 数据库 | MySQL 5.7 / 8.0 | 5.7 稳定,8.0 功能更多,选哪个都不影响主体设计 |
| 缓存(可选) | Redis | 可用来缓存公告、排课列表,也可只用 JWT 不引入 |
| 前端后台 | Vue 3 + Element Plus | Vue 3 配合 Element Plus,组件丰富,适合后台管理系统 |
| C 端 | 微信小程序原生 | 原生就够了,不要为了复杂度引入 uniapp |
| 接口文档 | Knife4j 或 SpringDoc | 自动生成接口文档,写文档、答辩演示都好用 |
为什么 Spring Boot 选 2.7.x 而不是 3.x?因为 3.x 基于 JDK 17,而大多数高校机房、毕设评审环境还在 JDK 8/11。如果你选 3.x 写成“JDK 17 + Jakarta 命名空间”那套,很多同学自己在配置环境时就会卡很久。2.7.x 在 2024 年仍然有丰富资料,也足够支撑这个项目的所有功能。 等到你答辩时如果老师问“为什么不用 3.x”,你就说“考虑到服务器环境和第三方生态兼容性,选择了更稳定的 2.7.x 版本”,这是一个很合理的工程决策。
Vue 后台项目用 JavaScript 还是 TypeScript?我的建议是 JavaScript。不是说 TS 不好,而是在毕设时间有限的前提下,JS 的上手成本和出错概率更低。TS 的类型约束虽然能帮你避免一些低级 bug,但对不熟练的同学来说,类型报错本身就会多花很多时间。项目核心是功能跑通,TS 等以后工作再用也不迟。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计:把核心关系理清楚
数据库设计是这类预约系统的灵魂。很多人的毕设论文字数堆不起来,很大一部分原因就是表设计不够细。反过来说,如果你能把表结构和字段逻辑讲清楚,光“数据库设计”这一章就能写出十几页扎实的内容。
2.1 数据表全景与 ER 关系说明
我先给出一套可以直接抄作业的表清单,这是完整版,实际开发时你可以按需裁剪:
| 表名 | 用途 | 核心字段 |
|---|---|---|
| user | 用户表 | openid, nickname, avatar, phone, status |
| admin_user | 管理员表 | username, password, real_name, role |
| coach | 教练表 | name, avatar, specialty, intro, status |
| course | 课程表 | name, type, cover, duration, max_count, status |
| schedule | 排课表 | course_id, coach_id, start_time, end_time, max_count, book_count, status |
| appointment | 预约记录表 | user_id, schedule_id, status, create_time, cancel_time, sign_time |
| announcement | 公告表 | title, content, publish_time, status |
| feedback | 反馈表(可选) | user_id, content, reply, create_time |
从 ER 关系的角度看,核心链路是:用户 -> 预约记录 -> 排课 -> 课程/教练。 用户和排课是多对多关系,通过预约记录这张中间表关联。一门课程可以有多个教练任教,一个教练也可以带多门课程,排课表实际上把“某教练在某时间上某课程”这个事实定下来了。
这里特别说一句,很多初学者容易把“课程”和“排课”混在一起。课程是模板,比如“动感单车”“瑜伽基础”;排课是实例,比如“周一 19:00-20:00 李教练的动感单车课”。用户预约的对象一定是排课,而不是课程。这个区分搞清楚,后面所有接口逻辑就不会乱。
2.2 关键表字段设计(课程表、排课表、预约表)
课程表(course)的设计相对简单,但有几个字段要想清楚:
max_count建议放在课程表还是排课表?我的答案是两边都放。课程表里有一个默认的上限,比如单节团课默认 20 人;排课表里也放一个max_count,排课时可以覆盖默认值。这样不同场次可以灵活调整人数限制。- 课程类型
type用字符串还是枚举?建议直接用字符串,比如“团课”“私教”“器械区”等,后台用下拉框限定可选值即可。用数字枚举虽然规范,但在小程序端、管理端和数据库之间传值反而增加解释成本。
排课表(schedule)是设计重点:
start_time和end_time统一用datetime类型,不要拆成date+time两个字段。拆开之后计算时长、判断时间冲突都要 concat 回来,多此一举。book_count表示当前已预约人数。这个字段属于冗余字段,因为它可以由 appointment 表统计出来。但之所以保留,是因为预约时更好做并发控制和列表展示。后面我会详细讲并发下的更新姿势。- 状态
status我建议用数字:0 未开始,1 进行中,2 已结束,3 已取消。方便定时任务批量更新。
预约记录表(appointment)最关键,字段逻辑如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| user_id | bigint | 用户 ID,加普通索引 |
| schedule_id | bigint | 排课 ID,加普通索引 |
| status | tinyint | 0 已预约,1 已完成,2 已取消,3 已爽约 |
| create_time | datetime | 预约时间 |
| cancel_time | datetime | 取消时间(可空) |
| sign_time | datetime | 签到时间(可空) |
这张表上一定要加一个唯一索引:UNIQUE KEY uk_user_schedule (user_id, schedule_id)。这是防重复预约的数据库兜底方案。代码里可以再查一次,但数据库索引是最后一道防线,必须加。
2.3 一个容易踩坑的并发细节:预约人数怎么扣
预约系统最常见的并发问题就是“超卖”——最后几个名额被多人同时抢到。如果代码写成这样:
java复制Schedule schedule = scheduleMapper.selectById(scheduleId);
if (schedule.getBookCount() < schedule.getMaxCount()) {
schedule.setBookCount(schedule.getBookCount() + 1);
scheduleMapper.updateById(schedule);
// 插入预约记录
}
在并发量稍微大一点的情况下就会出现幺蛾子:A 和 B 同时查到 book_count = 19,max_count = 20,两个都认为还有名额,都执行了 +1,最后数据库里变成 21 人预约,但实际只有 20 个名额。
正确做法是用带条件的 UPDATE 语句原子扣减,把“检查人数”和“扣减/增加人数”合并成一条 SQL:
sql复制UPDATE schedule
SET book_count = book_count + 1
WHERE id = #{scheduleId}
AND book_count < max_count
AND status = 0
MyBatis-Plus 里可以这样写:
java复制int rows = scheduleMapper.update(
new LambdaUpdateWrapper<Schedule>()
.eq(Schedule::getId, scheduleId)
.eq(Schedule::getStatus, 0)
.apply("book_count < max_count")
.setSql("book_count = book_count + 1")
);
if (rows == 0) {
throw new BizException("该课程已约满或已结束");
}
当 update 返回的影响行数为 0 时,说明没有抢到名额,不插入预约记录。只有 update 成功(rows = 1)才插入 appointment。如果这两步之间还担心失败,就用 @Transactional 包住,并把整个方法加锁。其实到 UPDATE 这一步已经足够安全了,因为 MySQL 行级锁会保证同一时刻只有一个人能更新成功。
取消预约反过来,用 setSql("book_count = book_count - 1"),并且要判断当前状态是不是“已预约”。只有已预约状态的预约才能取消,取消后释放名额。
3. 后端实现:Spring Boot 框架搭建与核心接口
3.1 项目分层与工程结构
后端工程我建议按常见的三层结构来组织,既清晰又好答辩:
code复制src/main/java/com/example/gym/
├── config # 配置类(CORS、拦截器、MyBatis-Plus分页插件)
├── controller # 接口层
│ ├── admin # 管理端接口
│ └── app # 小程序端接口
├── service # 业务层
│ └── impl
├── mapper # MyBatis-Plus Mapper接口
├── entity # 数据库实体类
├── dto # 请求/响应对象
├── common # 统一返回结果、异常处理、工具类
└── GymApplication.java
controller 层只做参数接收和转发,不要写业务逻辑。业务逻辑全部放在 service 层,这样写单元测试、加事务、复用代码都方便。这个小细节在答辩时也会被问到,提前养成好习惯。
统一返回格式建议用带泛型的结果类:
java复制public class Result<T> {
private Integer code; // 200 成功,500 失败,401 未登录
private String message;
private T data;
}
加一个全局异常处理器 @RestControllerAdvice,把业务异常 BizException 和系统异常分开处理。这样接口层不会到处写 try-catch,代码看着干净,别人读源码时好感度会大幅上升。
3.2 用户登录与 JWT 鉴权流程
小程序端没有传统意义上的“账号密码登录”。标准流程是:
- 小程序端调用
wx.login()获取临时凭证code - 把
code传给后端接口/api/app/auth/login - 后端拿
code请求微信官方提供的jscode2session接口,换取openid和session_key - 后端用
openid去user表查用户,查不到就自动注册新用户 - 生成本系统自己的 JWT token,返回给小程序
- 小程序后续所有请求都在 header 里带
Authorization: Bearer <token>
java复制public String login(String code) {
// 1. 请求微信接口,代码省略
String openid = wechatService.code2Session(code);
// 2. 查找或创建用户
User user = userMapper.selectOne(
new LambdaQueryWrapper<User>().eq(User::getOpenid, openid));
if (user == null) {
user = new User();
user.setOpenid(openid);
user.setNickname("微信用户");
userMapper.insert(user);
}
// 3. 生成 JWT
String token = JwtUtil.generateToken(user.getId(), user.getNickname());
return token;
}
这里有一个容易忽略的细节:JWT 里放什么信息?我建议只放 userId 和 nickname,不要放角色、手机号等可能变化的字段。因为 JWT 本身是自包含的,如果信息变更(比如用户改了昵称),旧 token 还会保留旧值,反而造成数据不一致。每次请求从 token 里取出 userId,再用 userId 去库里查最新用户信息即可。
管理端的登录逻辑不一样,直接用 username + password,密码在数据库里存的是 BCrypt 加密后的密文,不要用 MD5。MD5 撞库太容易了,答辩时也会被老师挑刺。Spring Security 太重的话,只引入 spring-security-crypto 这个包就能用 BCrypt 加密。然后用一个 AdminAuthInterceptor 拦截 /api/admin/** 下的请求,校验 token 并判断角色是否为管理员。
3.3 预约核心接口的状态机与事务处理
预约记录的状态一共有四种:已预约、已完成、已取消、已爽约。它们之间的流转关系:
- 已预约 -> 已取消(用户主动取消,取消后释放名额)
- 已预约 -> 已完成(上课时间结束,签到过或未签到但没取消)
- 已预约 -> 已爽约(上课时间已过但用户未签到且未取消)
- 已取消、已完成、已爽约 -> 终止状态,不可再流转
用户在“已预约”状态下发起取消预约的接口如下:
java复制@Transactional(rollbackFor = Exception.class)
public void cancelAppointment(Long appointmentId, Long userId) {
// 1. 查预约记录,校验归属
Appointment appointment = appointmentMapper.selectById(appointmentId);
if (appointment == null || !appointment.getUserId().equals(userId)) {
throw new BizException("预约记录不存在");
}
if (appointment.getStatus() != 0) {
throw new BizException("当前状态不可取消");
}
// 2. 取消预约,释放名额
appointment.setStatus(2);
appointment.setCancelTime(LocalDateTime.now());
appointmentMapper.updateById(appointment);
scheduleMapper.update(null, new LambdaUpdateWrapper<Schedule>()
.eq(Schedule::getId, appointment.getScheduleId())
.apply("book_count > 0")
.setSql("book_count = book_count - 1"));
}
这里两个操作必须放在同一个事务里。如果不加事务,就存在“预约记录标记取消了,但名额没释放”的情况。@Transactional(rollbackFor = Exception.class) 是标配,一定要写上 rollbackFor,否则只在运行时异常时回滚,自定义异常不一定触发回滚。
“已完成”和“已爽约”的状态更新,我建议用 Spring 的定时任务,每天凌晨跑一次:
java复制@Scheduled(cron = "0 5 0 * * ?")
public void autoFinishAppointments() {
// 找出所有 start_time < now 且 status = 0 的预约
// 如果 sign_time 不为空 -> 已完成
// 如果 sign_time 为空 -> 已爽约
}
也可以把状态更新的 SQL 在查询时动态计算,不一定非要用定时任务。但定时任务的方案更直观,也方便在文档里画流程图。
4. 前端双端实现:小程序与 Vue 后台
4.1 小程序端页面规划与关键逻辑
小程序端的页面结构我建议这样规划:
code复制pages/
├── index/ # 首页:公告轮播 + 课程分类 + 推荐课程
├── course/ # 课程列表:按类型筛选
├── course-detail/ # 课程详情:教练信息、排课时间列表
├── booking/ # 提交预约页面:确认排课时间、用户信息
├── order/ # 我的预约:状态筛选 + 操作按钮
├── profile/ # 个人中心:头像昵称、手机号
└── login/ # 登录页(首次进入或授权失效)
首页加载课程列表时,后端一次返回课程基本信息 + 近几天的排课情况。小程序端用 wx.request 请求后端接口,为了后续方便,建议统一封装一个 request.js:
javascript复制const request = (url, method = 'GET', data = {}) => {
return new Promise((resolve, reject) => {
wx.request({
url: baseUrl + url,
method,
data,
header: {
'Authorization': 'Bearer ' + wx.getStorageSync('token')
},
success: (res) => {
if (res.data.code === 200) {
resolve(res.data.data)
} else if (res.data.code === 401) {
wx.navigateTo({ url: '/pages/login/login' })
reject(res.data)
} else {
wx.showToast({ title: res.data.message, icon: 'none' })
reject(res.data)
}
},
fail: reject
})
})
}
小程序端最核心的交互是“选择排课时间并完成预约”。流程是:进入课程详情页 -> 加载该课程未来 7 天的排课列表 -> 用户点选某一时段 -> 弹出确认框 -> 调 /api/app/appointment/create 接口 -> 成功后跳转到“我的预约”页。
这里用户体验上有一个小细节:如果某节课已经约满,列表项要置灰并显示“已约满”,用户不能点击。这就要求后端在返回排课列表时,除了 book_count 和 max_count,还要多算一个 booked 布尔值,表示当前登录用户是否已经约过这节课。已经约过的排课应该显示“已预约”,点击后跳转到订单详情而不是发起重复预约。
4.2 小程序动态标题、授权手机号与备案备注
部分热搜词里提到的“小程序动态设置标题”确实属于实战会碰到的需求。比如课程详情页需要把顶部标题改成课程名称:
javascript复制wx.setNavigationBarTitle({
title: courseName
})
这个接口在使用上没什么坑,唯一要记住的是:它只能在页面加载后动态调用,不能替代 app.json 或页面 json 里的导航栏配置。比如你在 onLoad 里根据页面参数动态设置标题,就能让多个课程详情页共用一个页面组件,看起来像独立页面。
另一个值得一提的点是“用户手机号授权”。2023 年后微信官方收紧了手机号快速验证的规则,以前体验好的 getPhoneNumber 接口已经调整,需要认证的小程序才能使用。毕设阶段处理思路是:不强制绑定手机号,只通过微信登录(openid)建立用户身份,手机号做成可选的“补充信息”。如果你确实要收集手机号,可以用 input 组件让用户手动输入,这样不依赖企业认证,也能把功能做完整。
关于发布上线时的“小程序备案备注信息怎么填”,简单提醒一句:现在微信小程序上线必须先完成 ICP 备案。备案时服务内容分类选“工具 -> 预约/查询”,备注里就写“为用户提供健身房课程信息展示与预约服务”。如果你想省事,也可以选择不发布,只在开发者工具和真机调试阶段演示,但这样就无法生成线上版本,只能以“开发版体验”的方式给老师演示。通常毕设用开发版演示就够了,有条件再把备案流程走完。
4.3 Vue 后台管理:CRUD 之外还要做统计
Vue 后台我推荐用 Vue 3 + Vite + Element Plus。如果用 Vue CLI 创建项目,注意 Node 版本不能太低,Vite 5 建议 Node 18+。这里我直接说一下后台几个关键页面的实现思路。
课程管理页是最典型的 CRUD 页面。表格展示课程列表,弹窗表单做新增/编辑,status 字段用 el-switch 做上架/下架。表单校验用 Element Plus 自带的 rules 就够了。页面写完后,你会发现这类后台的代码模式高度相似。为了减少重复劳动,你可以抽一个 search-page 通用组件,把“搜索栏 + 表格 + 分页 + 弹窗”抽象出来。这也算一个亮点,文档里可以写“通过抽取通用列表组件,提升了开发效率”。
排课管理页相对复杂一点。新增排课时,要选择课程、选择教练、设置开始时间和结束时间,还需要校验“同一教练在同一时间段不能重复排课”。这个校验可以在后端做:
java复制public void createSchedule(ScheduleDTO dto) {
Long count = scheduleMapper.selectCount(
new LambdaQueryWrapper<Schedule>()
.eq(Schedule::getCoachId, dto.getCoachId())
.eq(Schedule::getStatus, 0)
.and(w -> w
.le(Schedule::getStartTime, dto.getStartTime())
.ge(Schedule::getEndTime, dto.getStartTime())
.or()
.le(Schedule::getStartTime, dto.getEndTime())
.ge(Schedule::getEndTime, dto.getEndTime())));
if (count > 0) {
throw new BizException("该教练在此时间段已有排课");
}
// 插入排课
}
这个 SQL 表达的是“时间段有重叠”,两个时间段互相交叉的情况都覆盖了。很多同学第一次写的冲突判断只覆盖了一种情况,结果漏掉了“新排课完全包含在旧排课里”的场景。
数据统计页是拉开差距的地方。除了基础的预约记录列表,至少做一个“近 7 天预约量折线图”。ECharts 配合 vue-echarts 组件,代码量不多,但演示效果非常加分。统计接口可以这样设计:
sql复制SELECT DATE(create_time) AS date, COUNT(*) AS count
FROM appointment
WHERE create_time >= #{startTime}
GROUP BY DATE(create_time)
ORDER BY date
后端返回一组 { date: "2024-05-01", count: 23 } 数组,前端直接映射到折线图的 x 轴和 y 轴。如果还有余力,可以做“课程预约热度 Top5”柱状图,用 course_id 分组统计。
5. 常见问题与避坑实录
这部分内容是我认为整篇最值钱的地方。项目开发和调试过程中,我踩过不少坑,也帮人排查过很多问题,把高频问题整理成速查表,方便你边做边对。
5.1 环境配置与版本兼容问题
问题 1:Spring Boot 版本太高导致 MyBatis-Plus 无法启动
这是“springboot版本太高”这个热搜词背后的典型场景。如果你用了 Spring Boot 3.x + MyBatis-Plus 旧版本,启动时会报 ClassNotFound: javax.annotation.Resource 之类的错误。原因很简单:3.x 引入了 jakarta 命名空间,旧版 MyBatis-Plus 还依赖 javax。
我的建议是:Spring Boot 统一用 2.7.x,MyBatis-Plus 用 3.5.3+,Druid 用 1.2.20+。如果已经用了 3.x,那就把 MyBatis-Plus 升级到适配 Spring Boot 3 的版本(注意 groupId 可能变成 com.baomidou:mybatis-plus-spring-boot3-starter)。别恋战,直接统一版本是最省时间的。
问题 2:Maven 依赖下载缓慢或失败
解决办法是配置阿里云镜像。在 settings.xml 中加上:
xml复制<mirror>
<id>aliyunmaven</id>
<mirrorOf>central</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
国内直连 Maven 中央仓库的体验确实不好,配好镜像能省大量时间。
问题 3:数据库连接报时区错误
连接 URL 里一定要加时区参数:
code复制jdbc:mysql://localhost:3306/gym_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
不加 serverTimezone 会报 The server time zone value... 或直接连接失败。同时在创建数据库连接时建议关闭 SSL 校验:useSSL=false。
问题 4:Vue 项目安装依赖时由于 Node 版本不匹配失败
Vite 5 需要 Node 18+。如果本地 Node 是 16,安装时会报 Unsupported engine 警告,虽然可能强制装成功,但运行时会各种报错。装个 nvm 管理 Node 版本,切到 18 或 20 再 npm install。如果还是慢,就把 npm 源换成淘宝镜像:npm config set registry https://registry.npmmirror.com。
问题 5:小程序真机预览请求不到后端
本机调试时,小程序开发者工具默认不校验合法域名,所以没问题。但真机预览时,手机和电脑要在同一局域网,并且接口地址要填电脑的局域网 IP,不能填 localhost。
code复制// app.js 里的 baseUrl
const baseUrl = 'http://192.168.1.100:8080'
如果你在微信开发者工具里看到请求正常,真机上却一直转圈,多半就是这个原因。记得在 project.config.json 里把 urlCheck 设为 false,或者打开开发者工具“详情 -> 本地设置 -> 不校验合法域名”选项。
5.2 后端编码与业务逻辑问题
问题 6:MyBatis-Plus 查询时间范围不生效
有人会写:
java复制new LambdaQueryWrapper<Schedule>()
.eq(Schedule::getStatus, 0)
.between(Schedule::getStartTime, begin, end)
这里如果 begin 和 end 是 String 类型,MyBatis-Plus 也能自动转换,但建议直接用 LocalDateTime。还有一点,数据库字段名如果带下划线,实体类用驼峰命名,一定要确认 map-underscore-to-camel-case 配置打开,否则查询结果里会出现 createTime 为 null 的问题。
问题 7:@Transactional 不生效
常见原因有三个:
- 方法被
private修饰(Spring AOP 代理无法拦截私有方法) - 方法在同一个类中通过
this调用(代理对象没有经过方法入口) - 异常被方法内部 try-catch 吞掉了
预约、取消预约、排课这类写操作,一定要把 @Transactional 加在 public 方法上,并且确保没有吞异常。
问题 8:预约状态和时间判断混乱
判断用户能不能预约,需要同时考虑三个维度:
- 排课本身状态是“未开始”
- 当前时间在排课开始之前(至少提前 30 分钟)
- 该用户没有预约过这个排课,且排课没满
这个逻辑不难,但容易漏判断。建议把“校验可预约性”抽成一个独立方法,比如 validateCanBook(schedule, userId),在创建预约接口里调用。接口看着简单,但里面的守卫条件反而是核心价值。
问题 9:小程序端日期时间显示差距 8 小时
后端 LocalDateTime 序列化到前端显示,经常出现时间少了 8 小时或 T 分隔符的问题。统一用 Jackson 配置解决:
properties复制spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
如果返回值是 LocalDateTime 类型,建议加 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")。这样小程序端拿到的就是格式化好的字符串,不需要再做处理。
5.3 答辩准备与文档撰写建议
毕业设计和商业项目的最大区别是:你需要让老师看懂你做了什么、为什么这么做。文档排版再花哨,也不如把核心思路讲清楚。
写论文时,数据库设计章节不要只罗列建表 SQL,要给 ER 图,并逐个表解释字段含义和设计理由。重点说清楚为什么要有排课表、为什么预约记录要加唯一索引、为什么人数控制要用条件更新而不是先查再改。这些设计决策就是分水岭。
功能和接口章节,建议做两个表:
- 功能-模块对照表(模块名称、功能描述、对应页面/接口)
- 接口清单表(接口路径、请求方式、参数说明、返回结果)
答辩演示时,建议准备两条演示路径。第一条走正常流程:登录 -> 浏览课程 -> 预约 -> 查看预约记录 -> 管理端查看预约数据。第二条走异常流程:连续预约同一节课,提示“不可重复预约”;将课程预约满后,其他账号预约提示“已约满”。异常流程的演示比正常流程更能体现系统的健壮性。
文档下载量和“源码+数据库+文档”这个搭配是很多人找毕设项目的核心诉求,但我想多说一句:网上有很多现成的源码包,但如果你不打算彻底吃透,只是改了名字就提交,答辩一问就露馅。尽量用里面提供的文档作为骨架,然后自己亲手把核心流程走一遍,把关键接口的代码读一遍,至少能讲清楚“预约”和“取消预约”这两条链路。这才是毕设最稳妥的过法。
5.4 数据库文档与后续扩展
最后聊一下数据库这块。你既要提供建库建表 SQL,又要提供完整的数据库设计文档。推荐把初始化脚本分成四个文件:
1_schema.sql:建库、建表语句2_data.sql:测试数据(课程、教练、排课示例)3_index.sql:索引和唯一约束4_admin.sql:管理员初始账号
测试数据非常重要。管理端和用户端如果没有任何示例数据,演示效果会非常差。多造几组课程数据、教练数据、排课数据,预约记录也造一些不同状态的,方便展示“我的预约”界面下的筛选功能。
这个项目的扩展空间其实很大。如果你还有精力,可以往这几个方向加功能:用户签到积分系统、课程评价模块、黑名单机制(多次爽约限制预约)、后台数据看板(预约转化率)。但记住一条原则:先保证核心流程稳定跑通,再谈扩展。很多同学项目做到一半就死在各种花哨功能的 bug 里,最后连最基础的登录都没演示好,非常可惜。
从我个人的经验来看,路线图的优先级应该是:登录、课程列表、预约/取消预约、后台管理、统计看板。这五块都能稳定工作,你的毕设就已经超过大多数人了。接下来再去打磨细节,比如空状态设计、loading 态提示、异常情况下的用户反馈,这些锦上添花的部分会明显提升演示观感。
无论是自己完成还是参考了别人的源码,都建议你把核心代码吃饭一样嚼透了再交。预约系统这类项目最大的魅力就在于,它把一堆看似基础的技术组合在一起,却能做出一个真实可用的产品。这个过程中的收获,远远不止一个毕业设计分数。
