拿这套“SpringBoot + Vue + 微信小程序健身房预约系统(源码 + 数据库 + 文档)”当毕设或者练手项目的同学,最常问我的三个问题分别是:这项目到底能不能跑起来、二次开发和答辩的时候我该从哪个模块入手、以及这套技术组合在当前就业环境下还有没有含金量。这篇文章就一次性把这些问题讲透。
先说结论:这个项目是非常标准的前后端分离 + 小程序端三端联动架构,后端是 SpringBoot 单体应用,前端是 Vue 管理后台,C端是微信小程序。技术上覆盖了主流企业开发的核心链路,而且健身房预约这个业务场景足够具体,不是那种烂大街的“通用后台管理系统”,在答辩和简历上都有话可讲。
1. 为什么选这套技术栈,以及它到底能做什么
1.1 技术选型背后的逻辑
我见过太多毕设项目,一上来就是 SpringCloud 全家桶加 Kafka 加 Redis 集群,结果连基本的增删改查都写得磕磕绊绊。实际上,中小规模项目选型的第一原则是:团队最熟悉什么、业务最需要什么,而不是什么火选什么。
这套项目选 SpringBoot + Vue + 微信小程序,是经过权衡的:
- SpringBoot 是目前 Java 后端就业市场绝对的主流,SSH(Struts + Spring + Hibernate)时代的东西在面试中基本只会被用来考古。SpringBoot 的自动配置、Starter 机制、内嵌 Tomcat,让开发者能十分钟起一个可跑的 Web 服务,对毕设和中小型商业项目都非常合适。
- Vue 在前端框架里属于“上手曲线平缓、生态成熟”的那一类。Element UI 组件库拖拖拽拽就能拼出一个后台管理系统,对后端同学来说非常友好。
- 微信小程序 则是C端触达成本最低的方式,不用下载 App,扫码即用,而且微信支付的接入流程对个人开发者来说也是相对成熟的路径。
这套组合在国内企业管理类、预约类、商城类项目中非常常见,学好这套技术栈,基本就摸到了中小型公司全栈开发的门槛。
1.2 功能地图:这套系统覆盖了哪些业务场景
健身房预约的核心业务链路是:用户发现课程 → 查看排期 → 预约时段 → 教练确认 → 到场核销 → 评价沉淀。围绕这条链路,系统应该拆成三个端来承载功能:
- C端(微信小程序):用户注册登录、浏览课程/教练、查看课程排期、提交预约、我的预约列表、取消预约、个人资料。这是会员每天打开的那个端,讲究的是操作路径短、页面清爽。
- B端(Vue管理后台):管理员管理教练信息、维护课程分类与课程模板、配置每周排期、查看所有预约记录、处理取消申请、管理会员列表、数据统计(预约量/课程热度/会员增长)。这个端的核心价值是“让运营可以不用写SQL就能干活”。
- 后端(SpringBoot接口层):为小程序提供 JSON API,为管理后台提供 REST API,统一做参数校验、权限校验(JWT)、异常处理和业务规则校验。
1.3 技能收获:做完这个项目你能学到什么
除了“会跑起来”这个基本目标,我更建议你做这个项目时带着这些学习目标:
- 理解前后端分离模式下接口文档(Swagger)的价值和协作方式;
- 掌握 SpringBoot 项目中分层架构(Controller/Service/Mapper)的职责边界;
- 知道 JWT 为什么比 Session 更适合小程序端的无状态认证;
- 了解排期类业务中“唯一约束 + 状态机”的设计思想,这是预约系统最容易被扣分的点;
- 掌握常规的部署方案:前端打包后交给 Nginx,后端打 Jar 包交给服务器,数据库用 MySQL 8.0。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 项目结构规划:三端工程怎么组织不吵架
2.1 工程目录的顶层设计
很多同学拿到源码工程后第一反应是“这是啥,从哪看起”。其实清楚了目录结构之后,你就能像看一本书的目录一样看懂项目。
规范的推荐结构是这样的:
code复制gym-reservation/
├─ server/ # SpringBoot 后端工程
│ ├─ src/main/java
│ ├─ src/main/resources
│ └─ pom.xml
├─ admin-web/ # Vue 管理后台工程
│ ├─ src
│ ├─ package.json
│ └─ vite.config.js
└─ miniapp/ # 微信小程序工程
├─ pages
├─ utils
├─ app.js
└─ project.config.json
用这种“一个总目录 + 三个子工程”的方式组织,最大的好处是把不同技术栈的代码隔离得清清楚楚,后续打包部署各管各的,不用在一棵树上互相纠缠。如果你用 Git 管理,甚至可以直接把三个子工程分别建仓库,用 submodule 关联起来。
2.2 后端包结构:让代码路径和业务一一对应
后端 Java 工程内部,包结构的合理性直接决定后期维护的心智负担。推荐按业务模块分包,而不是按技术层分包:
code复制com.gym.reservation
├─ config/ # 配置类:跨域、JWT、Swagger、MyBatis
├─ controller/ # 接口层:接收参数、返回统一响应
├─ service/ # 业务层:核心业务规则引擎
│ └─ impl/
├─ mapper/ # 数据访问层:MyBatis Mapper接口
├─ entity/ # 数据库实体类
├─ dto/ # 接口传输对象(vo/param)
├─ common/ # 通用返回类、异常、常量、工具类
└─ interceptor/ # 拦截器:登录认证、管理员权限
这里有一个很多人忽略的设计细节:entity 和 dto 要分开。不要图省事直接把数据库实体类丢给前端。理由很简单:
- entity 对应数据库表字段,为了查询效率,可能需要多表联查时的额外字段;
- dto 是给前端看的数据结构,比如预约列表里需要展示“剩余名额”“课程封面图”“教练头像”,这些数据在数据库里分散在多张表,用专门的 DTO 集中组装,接口返回的数据才干净可控。
- 直接返回 entity 还会带来敏感字段泄露风险,比如用户密码 hash、内部备注字段,一旦你哪天忘了写查询条件,问题就大了。
我见过不少项目把所有代码堆在 controller 里,一个方法两三百行,这样的代码“能跑”但是“不能改”。哪怕毕设是只需要演示一两次的东西,也请尽量写好结构,因为答辩老师说“我来改一个需求你当场实现”的概率比你预想的高。
2.3 Vue后台的目录套路
管理后台的目录相对固定,Vue 项目基本都长这样:
code复制src/
├─ api/ # 接口请求封装,按模块划分
├─ assets/ # 静态资源
├─ components/ # 通用组件
├─ router/ # 路由配置
├─ store/ # 全局状态(Pinia/Vuex)
├─ views/ # 页面级组件
└─ utils/ # 工具函数:request封装、token管理
重点说下 api/ 目录。不要把 axios 请求直接写在 .vue 文件里,统一抽一层的好处是:如果后端接口地址变了,你只需要改一个文件;如果要做请求拦截(比如自动带 token、统一错误提示),也在这一层解决。
2.4 小程序端的目录划分
小程序端的目录规划相对自由,但建议至少区分页面、组件、工具三个维度:
pages/: 每个页面一个目录,一个“页面”通常包含 .wxml、.wxss、.js、.json 四个文件;components/: 自定义组件,比如课程卡片、排期日历、空状态组件;utils/: 请求封装、日期格式化、登录态管理。
小程序一个很容易踩的坑是全局封装 request 时对登录态的处理。你要在请求拦截器里统一带上 token,并且在响应拦截器里统一处理 401 —— 跳转登录页。千万不要在每个页面的方法里单独处理登录失效,那样代码会爆炸且错误百出。
3. 数据库设计:预约系统的灵魂在约束,不在表多
3.1 核心表结构与设计思路
数据库设计是这套系统技术上最有含金量的部分之一,也是答辩老师最爱深挖的区域。下面是核心表的清单和设计要点:
| 表名 | 用途 | 关键字段与设计要点 |
|---|---|---|
member |
会员(小程序用户) | openid(微信唯一标识)、nickname、avatar、phone、status |
coach |
教练 | name、avatar、intro、specialty、status、sort |
course_template |
课程模板 | name、cover、intro、duration(分钟)、calories(消耗千卡)、max_count(人数上限) |
course_schedule |
课程排期 | template_id、coach_id、start_time、end_time、max_count、booked_count、status |
appointment |
预约订单 | member_id、schedule_id、appointment_no(单号)、status(0待上课/1已完成/2已取消/3爽约)、create_time |
admin_user |
后台管理员 | username、password(BCrypt加密)、real_name、role |
我没把“评价表”列进去,但你要是想加,也完全合理。评价表一般会有 appointment_id、member_id、score、content、reply 等字段,注意用预约记录 ID 做唯一约束,保证一个会员一次预约只能评价一次。
3.2 排期表设计隐藏的业务规则
course_schedule 是整个预约系统的枢纽表。它承载了教练、课程模板、时间三个维度的信息。这里有两个关键设计:
第一,通过 unique 约束防重复排期。 一个教练在同一时间段只能有一节课,可以在 (coach_id, start_time) 上建唯一索引。这样就算你并发来了两个请求同时为一个教练排课,数据库层也会拦下其中一个,而不是靠业务代码里先查后插——那种做法在并发场景下必翻车。
第二,已预约人数的记录策略。 我见过两种方案:
- 方案A:
course_schedule表保存booked_count字段,每次预约成功就booked_count + 1; - 方案B:不保存,每次查询时去
appointment表count(*)。
方案A在查询时性能更好,但要在事务中保证 booked_count 的递增和预约记录的插入是原子操作;方案B数据绝对准确,但每次列表加载都要做聚合查询,数据库压力大。实际项目一般选方案A + 事务 + 乐观锁(版本号)。你可以在 course_schedule 加一个 version 字段,更新时用 UPDATE ... SET booked_count = booked_count + 1, version = version + 1 WHERE id = ? AND version = ? AND booked_count < max_count。这一条 SQL 同时做了三件事:原子递增、并发控制、人数上限校验,用最少的代码解决最核心的并发问题。
3.3 一些容易忽略的字段和索引建议
- 所有业务表都应该有
create_time、update_time,建议由数据库自动维护(DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP)。 - 逻辑删除字段
deleted(0/1)建议保留,生产上尽量不要物理删除数据,因为你不知道哪天要回溯运营数据。 appointment_no要建唯一索引,用“日期 + 随机数”生成,比如202501151030128615。不要用自增 ID 当订单号展示给用户,那样会暴露你的业务量,且看起来不专业。- 索引不是越多越好。这个系统高频查询是“按日期查排期”“按会员查预约记录”“按课程查评价”,这三类查询建好索引就够。其他字段不要随意加索引,因为索引会影响插入性能、占用存储。
如果要提供 SQL 初始化脚本,建议按以下顺序执行:数据库建库 → 管理员表 → 会员表 → 教练表 → 课程模板表 → 排期表 → 预约表 → 插入初始数据(一个管理员、三五个教练、七八个课程模板、两周的排期)。初始数据一定要造得贴近真实,比如“19:00-20:00 的燃脂搏击课”,不要出现“123、456”这种一眼假的测试数据,答辩演示时细节非常加分。
4. 后端实现重点:SpringBoot核心链路逐段拆开
4.1 从零搭建 SpringBoot 项目的常用姿势
如果你拿到源码要先跑起来,只需要保证环境一致:JDK 1.8+、Maven 3.6+、MySQL 8.0+、IDE(IDEA 社区版完全够用)。
pom.xml 中建议引入以下依赖:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
MyBatis 需要你在 application.yml 里配置 Mapper 接口扫描和 XML 文件路径:
yaml复制mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.gym.reservation.entity
configuration:
map-underscore-to-camel-case: true
map-underscore-to-camel-case: true 是个救命配置——它让数据库的 create_time 自动映射到 Java 类的 createTime,省掉你在 XML 里手写几百个 resultMap。当然前提是数据库字段名按照下划线规范命名,Java 属性按照驼峰规范命名。
4.2 统一响应体和全局异常:写一次到处省心
后端接口返回给小程序和 Vue 的数据结构必须统一,否则前端要写一堆兼容逻辑。最简单的响应体如下:
java复制@Data
public class Result<T> {
private Integer code; // 200 成功,400 业务错误,401 未登录,500 系统错误
private String message;
private T data;
public static <T> Result<T> ok(T data) {
Result<T> r = new Result<>();
r.setCode(200);
r.setMessage("success");
r.setData(data);
return r;
}
public static <T> Result<T> error(String message) {
Result<T> r = new Result<>();
r.setCode(400);
r.setMessage(message);
return r;
}
}
配合全局异常处理器 @RestControllerAdvice,业务代码里你只需要 throw new BusinessException("该时段已约满"),前端统一从响应里取 message 弹出提示即可。这套组合是中小型项目的标配,极大地减少了 controller 里的 try-catch 样板代码。
4.3 预约 / 取消预约的核心业务逻辑
预约接口是系统性最强的代码,值得你反复读。核心逻辑我拆成三步:
第一步:参数校验。 排期 ID 不能为空,当前用户必须已登录(通过 JWT 解析出 memberId),排期存在且状态为“可预约”。
第二步:原子更新排期名额。 用上面提到的那条带乐观锁的 UPDATE SQL:
sql复制UPDATE course_schedule
SET booked_count = booked_count + 1, version = version + 1
WHERE id = #{scheduleId}
AND status = 0
AND booked_count < max_count
AND version = #{version}
执行后如果影响行数为 0,说明排期不存在、已下架或已满员,直接抛业务异常,事务回滚,预约失败。
第三步:插入预约记录并返回数据。 生成唯一单号,插入 appointment 表,返回预约详情给前端。
整个方法必须用 @Transactional,否则可能出现“名额扣了但订单没生成”的数据不一致事故。取消预约则相反:校验预约属于当前用户、状态为“待上课”,然后更新状态为“已取消”,同时把排期表的 booked_count - 1。
这里有一个很多人忽略的坑:已经开课的课程不能取消,要在代码里判断当前时间和 start_time 的关系,预留一个取消截止时间,比如开课前 30 分钟不允许取消。这个规则你在文档里写明,答辩时是可以拿出来讲业务思考的。
4.4 JWT 登录认证与拦截器配置
小程序端不能用传统的 Session 登录,因为微信小程序的请求环境特殊,而且官方明确不推荐在非 Web 环境维护 Session。主流做法是用 JWT(JSON Web Token):小程序端登录时把 code 发给后端,后端调微信接口换 openid,查库或注册会员,然后把 memberId、openid 这些信息加密生成 token 返回给小程序。之后小程序每次请求在 Header 里带 Authorization: Bearer token,后端拦截器解析 token 拿到当前用户。
下面是一个贴合课程表业务的实现要点:
java复制@Component
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 放行登录、注册、课程列表等公开接口
String uri = request.getRequestURI();
if (uri.startsWith("/api/member/login") || uri.startsWith("/api/course/list")) {
return true;
}
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new BusinessException(401, "请先登录");
}
String token = authHeader.substring(7);
// 解析 token、校验签名和有效期
// 解析出的 memberId 放入 request attribute,供 controller 使用
return true;
}
}
注意一个安全细节:退出登录时,最好把 token 加入黑名单(比如存 Redis 并设置剩余有效期),这样 token 在到期前就没法继续使用了。更简单的方案是前端直接丢弃本地 token,但严格来说这样服务端那边 token 依然有效。毕设如果不想引 Redis,也可以维护一张 token 黑名单表,但这属于加分项,不做也不影响核心功能。
4.5 接口文档:Knife4j 让答辩不用开 Postman
强烈建议集成 Knife4j(Swagger 增强方案)。只要在 controller 上加几个注解,项目启动后访问 http://localhost:8080/doc.html 就能看到所有接口的请求参数和返回示例,除了方便自己调试,答辩现场老师想看你接口的时候直接打开网页,比临时敲 Postman 显得专业太多。
java复制@Tag(name = "预约管理")
@RestController
@RequestMapping("/api/appointment")
public class AppointmentController {
@Operation(summary = "提交预约")
@PostMapping("/submit")
public Result<AppointmentVO> submit(@RequestBody @Valid AppointmentSubmitParam param) {
// ...
}
}
5. 管理后台和前端小程序:别忽略交互细节
5.1 Vue 管理后台:从接口封装到页面落地
Vue 后台的 src/utils/request.js 封装是第一个要看的文件。它要做的事包括:
- 创建 axios 实例,设置
baseURL为/api,这样配合 Vite 代理,开发环境下直接转发到后端 8080,避免跨域; - 请求拦截器添加 token;
- 响应拦截器统一判断
code,非 200 时弹出错误提示并 reject。
页面层面,管理后台至少包含:登录页、数据看板、教练管理、课程管理、排期管理、预约管理、会员管理。密码设置统一使用 BCrypt 加密存储,注意 Spring Security 的 BCryptPasswordEncoder 也可以单独拿出来用,不一定要引入完整的安全框架。
5.2 小程序端:登录流程、请求封装和页面设计
小程序登录是全流程中最容易出问题的地方,很多同学在“获取手机号”和“获取 openid”这两个概念上混淆。
一个常用的做法是:静默登录 + 手机号绑定。用户第一次进入小程序,通过 wx.login 拿到 code,发给后端换取 openid 并生成 token;此时用户其实已经可以浏览和预约基础功能了,但如果要绑定手机号(比如到店核销发短信或威客通知),再通过“获取手机号”按钮(bindgetphonenumber 事件)拿手机号加密数据,发给后端解密并保存。要注意获取手机号和获取 openid 是两回事:手机号需要用户主动点击按钮授权,openid 是每次 wx.login 都能拿到的用户唯一标识。
小程序端请求封装建议参考以下思路:
javascript复制// utils/request.js
const BASE_URL = 'https://your-domain.com/api'
const request = (url, method = 'GET', data = {}) => {
return new Promise((resolve, reject) => {
wx.request({
url: BASE_URL + url,
method,
data,
header: {
'Content-Type': 'application/json',
'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
})
})
}
页面上,预约流程应该做到“三步以内完成”,优选结构是:首页(今日热门课程) → 课程详情页(含排期时间表和预约按钮) → 提交成功页。不要为了让功能看起来多而把预约流程拉长,真实用户没有那个耐心。
5.3 时间选择组件的定制化
排期日历是预约系统的门面。你可以不用复杂的第三方日历组件,用一个横向滚动的“周视图”就够了:用户能看到本周每一天,点击某一天,下面展示当天的课程排期列表。实现这种通栏周切换的日历,小程序原生 scroll-view 加 WXML 循环就能搞定,不依赖额外库,跑起来也更轻快。
数据组装时,后端可以直接返回一个按日期分组的排期结构:
json复制{
"date": "2025-01-15",
"schedules": [
{"id": 1, "courseName": "燃脂搏击", "startTime": "19:00", "endTime": "20:00", "coachName": "李哲", "bookedCount": 8, "maxCount": 12, "status": 0}
]
}
前端拿到结构之后,只需要判断 bookedCount < maxCount 显示“预约”按钮,否则显示“已满员”。逻辑简单,用户体验也清晰。
6. 从源码到运行:最完整的复现步骤
如果你拿到的源码结构清晰,配置齐全,从零跑到能打开页面,最快只需要十分钟。下面是详细步骤:
6.1 后端启动清单
- 确认本地 MySQL 版本为 8.0+,创建一个数据库,名字随意,比如
gym_reservation,字符集选utf8mb4; - 导入项目附带的 SQL 文件,导入完成后你可以先执行几个查询,确认表都建好了、管理员初始数据存在;
- 修改
application.yml里的数据库账号密码,改成你自己本地的; - 确认 Redis 是否被用到:如果用到了,确保本地 Redis 已启动;如果没用到,直接忽略;
- 直接用 IDEA 打开
server文件夹(注意是 Maven 工程),等待依赖下载完毕; - 启动主类
GymReservationApplication,看到类似Tomcat started on port(s): 8080的日志,即启动成功。
我建议你启动后先访问 doc.html 测试“登录接口”,拿一个 token 再测“获取课程列表”,确认链路通了再关掉。
6.2 前端后台启动
- 用
cd admin-web进到目录,执行npm install(国内建议先设置镜像源,否则慢到你怀疑人生); - 执行
npm run dev,Vite 默认跑在5173端口; - 修改
vite.config.js中的代理配置,把/api代理到http://localhost:8080,避免开发环境跨域;
js复制server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
打开浏览器访问 http://localhost:5173,用初始管理员账号登录,你就能看到管理后台了。
6.3 小程序端运行
- 下载微信开发者工具,导入
miniapp文件夹(注意不是“新建项目”); - 在
project.config.json中确保appid是你自己的测试号(申请一个个人小程序即可); - 在
utils/config.js(或类似文件)里把接口地址改成http://localhost:8080/api,注意在开发者工具里勾选“不校验合法域名”,因为本地调试不走 HTTPS; - 编译运行,如果模拟器里能看到首页课程列表,说明联调成功。
6.4 常见启动失败问题速查
| 现象 | 大概率原因 | 解决办法 |
|---|---|---|
Access denied for user |
application.yml 数据库账号密码错误 | 改成你自己的 MySQL 账号密码 |
Unknown database |
数据库没创建,或库名不一致 | 执行 CREATE DATABASE gym_reservation DEFAULT CHARACTER SET utf8mb4; |
| Maven 依赖下载超时 | 未配阿里云镜像 | 在 settings.xml 里加 aliyun 镜像 |
端口 8080 被占用 |
本地有其他服务占用 | 改 application.yml 里的 server.port,记得连同前端代理一起改 |
小程序请求失败 url not in domain list |
本地调试未开“不校验合法域名” | 开发者工具右上角“详情” →“本地设置”中勾选 |
小程序登录报 401 |
token 没带/过期 | 检查 request 封装里的 header 是否带上了 token |
7. 实战踩坑记录:这些都是你看文档学不到的
7.1 同一个坑:微信的 code 只能换一次 openid
小程序登录接口里后端用 code 换 openid。很多人第一次做会踩这个坑:前端调了一个“预登录接口”拿到 session,又调一个“正式登录接口”再传一次 code,结果第二次调用时微信服务器返回 errcode: 40029(invalid code)。原因很简单:微信的 code 是一次性的,用过了就失效。所以拿到 code 之后,前端应该只在“登录”这个动作里用一次,不要为了多拿点数据反复调用。
我见过最稳妥的模式是:wx.login 拿到 code → 直接调 POST /api/member/login → 后端完成 openid 换发、注册/登录判断、token 签发三步操作 → 返回 token 和用户信息。一个接口解决登录,简洁干净。
7.2 并发预约测试:用 “无” 也得有测试方案
预约系统最核心的并发问题就是超卖(超过课程人数上限的预约成功)。你可以在本地起两个终端,用 curl 或者写一个小的线程池并发调预约接口。没有用乐观锁的实现,大概率会出现 booked_count 超过 max_count 的情况。用了前面说的版本号方式之后,并发 20 个请求进来,最终成功的数量一定等于剩余名额数。
这个现象特别适合写进你的项目说明文档,答辩时主动提“我做了并发控制”,比被动回答“没有做防超卖”要强非常多。
7.3 时间处理:时区问题能让你排查一小时
MySQL 连接串里一定要带 serverTimezone=Asia/Shanghai,否则插入时间会和本地时间差 8 小时。我见过太多次“预约时间是对的,但数据库里存的时间往后跑了8小时”的情况,最后查了半天发现是 JDBC 的时区配置问题。
另外,排期表里存的是“上课开始时间”,前端展示时建议统一使用后端返回的 String 格式(比如 "19:00"),不要在手机端做时区转换,否则各种边界情况会让你想砸电脑。
7.4 管理后台“排课”时的细碎体验
管理后台的排课功能,视觉上和操作上都有很多优化空间。最有价值的一个优化是:同一页显示一周七天 × 每天的课程时间槽,教练在哪个时段有空闲一目了然。如果做成一个纯列表让你一个时段一个时段地新增,运营人员会崩溃的。
实现上,这个页面需要后端提供“某教练在一周内的已有排课”数据,前端拿到后在日历网格上把冲突时段标红禁用。这个交互看起来只加了一点点逻辑,但对业务效率的提升是成倍的。
7.5 部署阶段:HTTPS 是绕不开的坎
小程序正式发布要求接口域名必须是 HTTPS,而且要在小程序后台配置合法域名。本地开发可以用“不校验合法域名”绕过,但上线不行。你的部署方案一般有两种:
- 方案一(推荐):买一台轻量云服务器,装 Nginx + MySQL + JDK,后端打 Jar 包用
nohup java -jar常驻运行,前端npm run build后把dist目录交给 Nginx 托管,用 Certbot 配一个免费的 HTTPS 证书。 - 方案二(省钱):用内网穿透把本地服务暴露到公网,配合一个免费域名,但这种方式稳定性一般,只适合临时演示。
部署时还有两个细节:Nginx 里要把 /api 路径反向代理到后端的 8080 端口;小程序端的 BASE_URL 要改成你最终部署的 HTTPS 域名,不要带着端口号。
8. 二次开发建议:想在答辩中脱颖而出就做这几点
如果你的时间和精力有余,下面几个方向是性价比比较高的二次开发点,每一个都能在答辩时讲出深度:
方向一:预约提醒通知。 在预约成功/开课前提醒上,你可以用微信订阅消息(一次性订阅消息)实现。用户在小程序里预约时弹窗请求订阅“开课提醒”,后端在排期开始前调用微信接口推送通知。这个功能非常贴合业务,且能展示你对微信生态的熟悉度。
方向二:数据统计看板。 管理后台首页的数据看板可以不用 ECharts 那种大而全的方案,用简单柱状图 / 折线图展示“近7日预约量”“热门课程 Top5”就行。后端做一个统计接口,用 GROUP BY 查出来,前端接数据渲染。别小看这个功能,它是很多企业后台真正会被高频使用的模块。
方向三:课程评价与教练评分。 在预约完成(到店核销)后引导用户评价,评价数据回流到教练页面。这个功能让系统从“单向预约工具”变成“带社区属性的服务平台”,业务完整性明显提升。
方向四:对接微信支付。 在线付费预约是很多真实健身房的需求,也是项目商业价值最有说服力的体现。不过要提醒一句,微信支付需要企业主体资质,个人开发者只能用测试号模拟支付流程演示。
9. 选型和实现之外的几点真心话
电脑里存着源码不是本事,能拆开、看懂、改得动才是。
我刚工作那几年,见过很多同学拿着别人的源码跑起来就觉得自己“会了”,答辩时老师问“这个表为什么这样设计”“预约并发怎么处理”“你改了哪些代码”,回答得支支吾吾。实际上源码本身只是你学习的起点,你要做的不是背代码,而是理解每一个模块存在的原因,踩一踩那些错误的分支,想一想如果换一个场景你会怎么调整。
做健身房预约这个项目,好的学习路线不是“跑起来 → 看代码 → 答辩”,而是“跑起来 → 拆功能 → 改功能 → 把改的过程写进文档 → 总结出设计理由”。只要你真正沿着这条路线走一遍,收获的绝不只是一份能过查重的文档,而是写进简历里经得起追问的项目经验。
另外说一个最实在的:不管你最后开发到什么程度,一定把 README.md 写清楚——如何导入SQL、如何启动后端、如何启动前端、默认账号是什么。这个文件不仅方便答辩老师快速复现你的项目,也是你“工程素养”的一种体现。很多项目功能做得不错,就是栽在“别人拿过去根本跑不起来”这种低级问题上,太可惜了。
这套系统的价值不在技术多前沿,而在于它把你学过的 Web 开发知识完整地串了一遍。串起来之后,你会发现那些上课时觉得抽象的概念——事务、拦截器、乐观锁、跨域、Token 认证——原来在真实业务里都有它不可替代的位置。把这个“原来如此”的瞬间记录下来,就是你在这个项目里最大的收获。
