☰
SpringBoot+Vue+微信小程序健身房预约系统设计与实现全解析
2026/10/28 18:06:11 网站建设 项目流程

简介:面向微信小程序的健身房预约管理系统毕业设计资源包,基于Java、SpringBoot、Vue与MySQL构建,采用前后端分离架构,覆盖用户注册登录、健身房信息查看、教练信息浏览、课程预约、课程管理、教练管理、系统设置等模块。适合高校计算机相关专业学生作为毕业设计、课程设计或期末大作业,项目已通过导师指导并验证,下载后在IDEA中使用Maven管理依赖,用Navicat导入数据库脚本,即可直接部署运行。资源包共1641个文件,大小16.69MB,包含179个Java后端源码、173个Vue前端组件、132个WXSS与WXML小程序页面、319个PNG图片,以及SQL数据库脚本、Maven配置和bat启动脚本,前端、管理后台、后端与数据库划分清晰。目前已有121人学习下载,适合需要完整可运行项目作为设计参考或二次开发起点的开发者。压缩包内除源码和数据库脚本外,还提供项目文档、工具配置与常用命令,通过目录即可定位各功能模块;系统经严格调试可稳定运行,便于答辩演示、功能扩展和深入研究。

1. 毕业设计里的健身房预约小程序:Java+SpringBoot+Vue+MySQL 这套组合为什么值得交付

打开这份标题为“基于微信小程序Java+SpringBoot+Vue+Mysql开发的健身房预约管理系统”的源码包,大多数人最关心三件事:代码能不能直接跑通、答辩时功能够不够讲、导师追问数据库和并发时能不能接住。这类选题之所以高频出现在毕业设计中,是因为技术栈刚好覆盖 Web 全栈的标配:微信小程序做用户端,Vue 做运营后台,SpringBoot 出接口,MySQL 存业务数据。它解决的场景也很真实——健身房排课、场地占用、会员预约和扣费,不是那种造出来的假需求。适合需要快速交付完整系统的人练习前后端分离,也适合把预约类逻辑迁移到别的行业复用。下面按我搭这类项目的顺序,把表结构、接口、联调和踩坑一次讲完。

2. 把预约业务拆成数据模型:MySQL 表设计与三个容易忽略的约束

2.1 先分清三种角色和两类预约场景

健身房预约如果只做一张订单表,后面改起来会很痛苦。常见做法是先按角色拆权限:普通用户(会员)在小程序端浏览课程、约课、看自己的订单;管理员在 Vue 后台维护场地和课程排期、查看场馆总览;教练在多数毕业设计里不单独登录,作为课程表上的属性存在,顶多再配一张教练信息表,方便后台选择。角色理顺之后,后端接口权限和前端菜单才不会乱。

场景上要区分两类预约。一类是场地预约,用户选某个时间段约羽毛球场地、器械区或者私教室,计费按小时,约束是同一时段同一场地不能重复占用。另一类是课程预约,动感单车、瑜伽、团操这类有容量上限的课,用户报名成功后扣减会员卡次数,名额满了就约不进。两者在订单表里可以合并,但业务判断完全不同:场地预约要查设施占用冲突,课程预约要扣课程剩余容量。如果设计时不分开考虑,后面写列表查询和统计报表会发现状态字段完全不够表达。

预约类系统的状态流转也要比普通 CRUD 多考虑一步。一张预约订单至少经历待支付、已预约、已完成、已取消、已过期这几个状态。过期状态是特别容易被遗漏的:用户下单不支付,名额被占住,后端没有自动释放逻辑,演示时会出现“名额显示已满但实际没人生成有效订单”的尴尬。状态值建议用数字存,但代码里必须写成枚举或常量,不然后期管理会混乱。

2.2 核心表结构:场地表、课程表、订单表、会员卡流水表

按我搭这类项目的习惯,核心表一般不少于六张。user 用户表存 openid、昵称、手机号、会员类型;coach 教练表存姓名、头像、简介、状态;facility 场地表存场地名称、位置、容纳人数;course 课程表存所属场地、教练、开始结束时间、容量、已约人数;appointment_order 订单表存订单号、用户、课程或场地、时间段、状态;member_card 会员卡表存卡类型、剩余次数、有效期;card_record 流水表记录每次扣次与退次。

user 表的 openid 字段必须加唯一索引,因为小程序登录天然以 openid 作为账号唯一标识。coach 表在演示环境可以先不参与主流程,但 Vue 后台的课程表单要选择教练,所以最好保留。场地表和课程表是一对多关系:一个场地在不同时段可以排多节课程,课程表用 facility_id 关联场地。订单表同时关联 user 和 course,为了兼容场地预约,course_id 做成可空或默认为 0,再补上 facility_id、appointment_date、开始和结束时间字段,不然场地预约无处落库。

会员卡和流水这两张表,我见过不少简化的做法是直接在订单上记录“扣了几次”。平时看不出问题,一旦要演示取消预约退款,就麻烦了:订单取消了,卡上的剩余次数怎么恢复?如果没有流水表,只能从订单状态反推,数据一多就不可靠。加一张 card_record 流水表,每次扣次、退次都写一条记录,逻辑上容易解释,答辩时也能把“预约扣费”这条业务链路讲完整。各表职责可以先看这张表,方便后面建 SQL 时对照:

表名作用关键字段关联关系
user微信用户与会员openid, phone, member_type被订单、会员卡引用
coach教练信息name, avatar, status被课程引用
facility场地信息name, location, capacity被课程引用
course课程排期start_time, end_time, booked_count关联场地与教练
appointment_order预约订单status, user_id, course_id关联用户与课程
member_card会员卡remain_count, expire_time关联用户
card_record扣次与退次流水change_type, count关联会员卡与订单

2.3 建表 SQL 落地:字段类型、索引与逻辑外键的取舍

可以直接拿这份建表 SQL 作为基础版本,注意注释里写的设计点就是后面排查问题的关键:

-- 课程表:booked_count 是冗余字段,列表页直接展示剩余名额 CREATE TABLE `course` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `facility_id` BIGINT NOT NULL DEFAULT 0 COMMENT '所属场地id', `coach_id` BIGINT NOT NULL DEFAULT 0 COMMENT '教练id', `course_name` VARCHAR(64) NOT NULL COMMENT '课程名称', `start_time` DATETIME NOT NULL, `end_time` DATETIME NOT NULL, `capacity` INT NOT NULL DEFAULT 20 COMMENT '课程容量', `booked_count` INT NOT NULL DEFAULT 0 COMMENT '已预约人数', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1可预约 0下架', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_facility_start` (`facility_id`, `start_time`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健身课程表'; -- 订单表:user_id 和 course_id 组合唯一,防重复预约 CREATE TABLE `appointment_order` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL COMMENT '业务订单号', `user_id` BIGINT NOT NULL, `course_id` BIGINT NOT NULL DEFAULT 0 COMMENT '课程id,场地预约可为0', `facility_id` BIGINT NOT NULL DEFAULT 0, `appointment_date` DATE NOT NULL, `start_time` TIME NOT NULL, `end_time` TIME NOT NULL, `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已预约 2已完成 3已取消 4已过期', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_course` (`user_id`, `course_id`), KEY `idx_user_status` (`user_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预约订单表';

这段 SQL 体现了三个容易忽略的约束。第一个是字符集,必须用 utf8mb4 而不是 utf8。微信用户名经常带 emoji,utf8 字符集存三个字节,遇到 emoji 会直接报 Incorrect string value 错误,这是非常经典的翻车点。第二个是冗余字段 booked_count,它让列表页不用每次都 count 订单表,但代价是并发更新时必须保证原子性,后面接口里不能先 select 再 update,而是直接 UPDATE 带容量条件。第三个是唯一索引,user_id 和 course_id 的组合唯一键是防重复预约的最后一道防线,哪怕代码里漏了判断,数据库也会拦下第二次插入。

外键的问题也要提前说。我一般不在建表语句里写物理外键 CONSTRAINT FOREIGN KEY,只保留逻辑字段。原因有两个:第一,毕业设计经常需要批量导入导出数据,物理外键会让导入顺序很挑剔;第二,有的同学会在删除课程时顺手把订单物理删除,对业务数据太危险。保留外键字段但不加物理约束,配合 Service 层校验,是预约类系统的常规做法。如果导师坚持要有外键概念,可以在论文 ER 图里画逻辑外键,并说明采用逻辑外键是为了保证数据灵活性和删除可控。

时间字段这里用 DATE 和 TIME 分开存,而不是把开始和结束时间拼在一个字符串里。分开的好处是统计“这个场地当天有哪些课”的时候,可以按 appointment_date 做范围查询,不用对字符串做函数处理。create_time 用 DEFAULT CURRENT_TIMESTAMP 让数据库维护写入时间,后端代码不要手动 new Date 赋值,避免多机部署时时间不一致。这套 SQL 在 MySQL 5.7 和 8.0 下都能跑,如果本地是 MySQL 8.0,唯一要注意的是字符集和时区参数,后面避坑部分会提到。

3. SpringBoot 后端:登录鉴权与预约下单的两个关键接口

3.1 项目依赖选择:SpringBoot 版本、MyBatis-Plus 与 JWT 的取舍

网上搜“springboot 健身房预约”能翻出一堆代码,但很多跑不起来,根子都在版本上。这套毕业设计后端最稳妥的组合是 SpringBoot 2.7.x、JDK8、MyBatis-Plus 3.5.x、MySQL 8.0、Hutool 工具包、JWT 做登录态。SpringBoot 3.x 虽然新,但把 javax 包换成了 jakarta,代码里到处都是 javax.annotation、javax.servlet 的 import,编译会直接失败;而且 SpringBoot 版本太高之后,3.x 还强制 JDK17,学校机房如果只装了 JDK8,答辩现场就是灾难。所以我一般会强调:技术选型不要追新,稳定优先。

MyBatis-Plus 的价值主要在于省掉大量单表 CRUD。BaseMapper 内置了 selectById、selectList、insert、updateById,配合 LambdaQueryWrapper 写条件查询,课程列表、订单列表这类接口很快就能写完。JWT 用来替代 session。小程序端没有 Cookie 机制,登录成功后拿到 token,后续请求放在 header 里传回来,后端用拦截器统一校验。这里有一个坑:如果直接抄网上的 jjwt 0.9 代码,Jwts.builder() 的 API 在新版本里已经大改,编译不过;建议直接用 hutool 的 JWTUtil,两行代码就能生成和解析。

application.yml 里的参数也会影响联调。MySQL 连接串必须带 serverTimezone=Asia/Shanghai,否则 8.0 驱动会报时区错误。数据库连接池如果是 HikariCP,maximum-pool-size 建议 10 到 20,太小并发请求时会排队,太大对学校低配服务器也不友好。Jackson 对 LocalDateTime 的默认序列化格式是数组,前端不方便处理,建议在 yml 里配置 spring.jackson.date-format=yyyy-MM-dd HH:mm:ss,并把 time-zone 也指到上海时区。这些参数不调对,代码写得再对也无法联调。

3.2 微信小程序登录:code 换 openid,手机号授权的正确姿势

@RestController @RequestMapping("/api/auth") public class AuthController { @GetMapping("/login") public Result login(@RequestParam("code") String code) { // 1. 拿 code 去微信接口换 openid String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appid + "&secret=" + secret + "&js_code=" + code + "&grant_type=authorization_code"; String res = restTemplate.getForObject(url, String.class); JSONObject obj = JSON.parseObject(res); if (obj.getInteger("errcode") != null) { return Result.error("登录失败,请重新进入小程序"); } String openid = obj.getString("openid"); // 2. 查用户表,不存在则自动注册 User user = userMapper.selectOne(new LambdaQueryWrapper<User>() .eq(User::getOpenid, openid)); if (user == null) { user = new User(); user.setOpenid(openid); user.setNickname("微信用户" + openid.substring(openid.length() - 4)); userMapper.insert(user); } // 3. 签发自己的 JWT,后续接口携带 String token = JwtUtil.createToken(user.getId(), "user"); return Result.ok().put("token", token).put("user", user); } }

这段代码是登录注册一体化的典型写法。微信小程序端 wx.login() 拿到临时 code 后,后端拿 code + appid + secret 去微信接口换 openid。注意微信返回的 openid 在响应体 JSON 里,不是 header。如果 code 已过期或重复使用,微信会返回 errcode 40029,所以这里要做错误处理,否则小程序端会收到一个 null 的 openid,大概率引发一个奇怪的查询异常。

手机号授权这里特别容易理解错。小程序端不能直接调“手机号登录”接口给后端一个手机号,而是要在页面上放一个 button,设置 open-type="getPhoneNumber",用户点击授权后微信返回加密数据,再由后端调用解密接口配合 access_token 换取真实手机号。这涉及小程序 AppSecret 管理和 access_token 刷新,毕业设计如果只是演示预约流程,建议不做真实解密,用 wx.getUserProfile 拿昵称头像,手机号留空或在个人中心里手动填写。强行接真实手机号,审核和资质要求都会增加不必要的成本。

JWT 签发之后,前端每次请求在 header 里带 token,后端拦截器解析 token 得到 userId,放进请求上下文。业务代码不要依赖前端传 userId,否则接口可以被随意伪造。token 有效期一般设 7 天,小程序用户登录一次基本够用;管理后台的 token 时效可以短一些,2 小时强制重新登录,避免安全风险。后端还需要处理 token 过期情况,统一返回 401,前端封装层收到 401 后跳转登录页。

3.3 预约下单接口:原子扣减库存与防重复提交

// 扣减库存,容量判断放在 UPDATE 条件里 int affected = courseMapper.update(null, new LambdaUpdateWrapper<Course>() .eq(Course::getId, courseId) .eq(Course::getStatus, 1) .apply("booked_count < capacity") .setSql("booked_count = booked_count + 1")); if (affected == 0) { throw new BusinessException("课程已约满"); } // 插入订单,唯一索引兜底重复预约 AppointmentOrder order = new AppointmentOrder(); order.setOrderNo(OrderNoGenerator.generate()); order.setUserId(userId); order.setCourseId(courseId); order.setStatus(0); // 待支付 try { orderMapper.insert(order); } catch (DuplicateKeyException e) { throw new BusinessException("您已预约过该课程"); }

这段代码最核心的是那个带容量条件的 UPDATE。很多人习惯先 select 查容量,再决定是否插入订单,这在单线程下没问题,并发一上来就超卖:两个请求同时读到同一个旧值,都觉得还有名额,最后都插入成功。把 booked_count < capacity 放到 update 的 where 条件里,让数据库的行锁来决定胜负,affected 等于 0 就说明名额已经被抢光。这是预约类系统里最值得优先做的第一道防线。

MySQL 锁的类型这时候需要拎出来讲清楚。上面这条 UPDATE 用的是 InnoDB 行锁,但行锁范围取决于 where 条件命中的索引。如果以主键 courseId 定位,锁的是这一行,性能最优;如果 where 里出现没有索引的字段,InnoDB 可能对多条数据加锁甚至锁表。所以 course 表的主键和 facility_id + start_time 联合索引必须建好,不然后端逻辑再对也被数据库拖垮。

防重复提交是第二道防线。用户手抖点两次预约按钮,前端只能拦截一部分,后端要能兜底。最简单可靠的方式就是前面 SQL 里的唯一索引:appointment_order 表建 user_id + course_id 唯一键,第二次插入抛出 DuplicateKeyException,捕获后返回“您已预约过该课程”。如果有 Redis,可以再加一个 setnx 锁,控制同一个用户一定时间内的请求频率;没有 Redis 的环境,只靠唯一索引也足够答辩用。订单插入后状态先置为待支付,模拟支付完成后再更新为已预约,主流程就完整了。支付不要接真实微信支付,缺少商户资质,演示用模拟支付按钮即可。

4. 微信小程序端与 Vue 管理后台:两套前端的职责划分和联调

4.1 小程序端:预约页、订单页与请求封装

小程序端一般包含四个核心页面:首页展示课程和场地列表,课程详情页展示时间、剩余名额和预约按钮,订单列表页展示预约历史,个人中心展示会员卡和登录状态。如果项目还支持场地预约,详情页可以加一个 tab 切换“课程预约”和“场地预约”。页面数量不是越多越好,把预约主流程走通,再加一个后台看板,基本就能覆盖毕业设计的演示时长。

请求封装要先给一个通用的 wx.request 包装,不然每个页面都写一遍请求逻辑,代码会非常散:

// utils/request.js const BASE_URL = 'http://127.0.0.1:8080/api'; function request(url, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method, data, header: { 'Content-Type': 'application/json', 'token': wx.getStorageSync('token') || '' }, success(res) { if (res.data.code === 401) { wx.navigateTo({ url: '/pages/login/login' }); reject(res.data); return; } if (res.data.code === 200) { resolve(res.data.data); } else { wx.showToast({ title: res.data.message, icon: 'none' }); reject(res.data); } }, fail: reject }); }); } module.exports = { request };

这段封装把 token 加到 header,code 为 401 时统一跳转登录页,业务页面里只需要关心业务数据。注意 wx.getStorageSync('token') 在未登录时返回空字符串,不要用 undefined 判断,否则 header 会带一个 “undefined” 字符串,后端解析 token 时报错。小程序端的接口地址统一放 config.js,不要在业务页面里散落字符串,后面换服务器只需要改一个文件。

这里补两个实际参数坑。微信小程序顶部导航栏高度在不同机型上不一样,如果不做自定义导航,直接用默认导航栏设置 title 最省事;如果非要自定义,不要写死像素,要用 wx.getMenuButtonBoundingClientRect() 得到胶囊位置再推算。wx.request 的 timeout 建议显式设置 10 到 30 秒,后端本地调试时有冷启动时间,默认超时配置可能让真机偶发请求失败。

4.2 Vue 后台:课程排期、管理员登录与看板

Vue 管理后台我一般选 Vue 2 + Element UI,或者 Vue 3 + Element Plus。Vue 3 语法更现代,但 Element Plus 对 npm 和 node 版本要求较新,如果环境版本低,npm install 会报一堆错误,答辩现场很狼狈。Vue 2 生态稳定,Element UI 组件文档虽然老,但正是这种“老”让它在各种环境中容易跑起来,对于毕业设计这种强调稳定的场景更合适。

后台核心页面有三块:登录页、课程管理页、订单管理页。课程管理要支持新增课程、调整时间、设置容量、上下架。表单里最关键的是日期时间选择器,下面这段是一个典型写法:

<template> <div class="course-form"> <el-form :model="form" label-width="120px"> <el-form-item label="课程名称"> <el-input v-model="form.courseName" /> </el-form-item> <el-form-item label="所属场地"> <el-select v-model="form.facilityId"> <el-option v-for="f in facilities" :key="f.id" :label="f.name" :value="f.id" /> </el-select> </el-form-item> <el-form-item label="开始时间"> <el-date-picker v-model="form.startTime" type="datetime" placeholder="选择开始时间" value-format="yyyy-MM-dd HH:mm:ss" /> </el-form-item> <el-form-item label="容量"> <el-input-number v-model="form.capacity" :min="1" :max="200" /> </el-form-item> <el-form-item> <el-button type="primary" @click="saveCourse">保存</el-button> </el-form-item> </el-form> </div> </template>

这段表单里 value-format 属性非常关键。不写的话,组件默认输出 Date 对象,请求体序列化后是时间戳,SpringBoot 的 LocalDateTime 字段接不住,直接报 JSON 解析失败。绑定场地和教练时,下拉选项数据要在 mounted 里从接口拉取,不要在 data 里写死。订单管理页展示所有预约记录,支持按课程、用户、状态筛选,已完成订单要禁用取消按钮,避免业务状态被随意回退。

4.3 双端 token 隔离与接口对接的常见错位

小程序端和管理后台是两套独立的登录体系。小程序用户用 openid 登录,签发 user_token;后台管理员用账号密码登录,签发 admin_token。两套 token 虽然都叫 token,但存储位置不同,最容易出错的是后端拦截器:如果两套 token 使用同一个密钥,且 claims 里没有区分角色,一个管理员 token 可能被当成用户 token 使用。我习惯在 JWT 里加一个 role 字段,后端拦截器先判断 role 再放行,双端互不干扰。

接口响应结构也要统一。全后端都应该返回 { code, message, data },不要让一个接口直接返回数组,另一个接口返回 Map。前端封装层统一判断 code 之后,业务代码里就不需要重复写异常分支。很多毕业设计里一个页面一个响应风格,联调时每个接口都要回看后端代码才能确定返回字段,效率很低。在动手写页面之前,先把课程列表、订单列表、预约下单这几个核心接口的路径和参数约定清楚,能让前后端联调少走一半弯路。

5. 避坑排查:从本地联调到答辩演示的五个高频问题

5.1 现象:小程序请求课程列表返回 500,后端日志出现 Access denied for user 'root'@'localhost'

原因:application.yml 里的数据库账号密码和本地 MySQL 不一致,常见于从别人那里复制的项目,密码没改。解决:确认本地 MySQL 的账号密码并同步到 yml 配置;如果用的 MySQL 8.0,还要检查连接串是否带 useSSL=false 和 serverTimezone,避免驱动握手阶段直接失败。这一步属于环境类问题,通常配置正确后立即恢复。

5.2 现象:两个账号同时预约只剩 1 个名额的课,双双提示成功

原因:后端先 select 容量再 insert 订单,并发下两次 select 读到同一个旧值,判断都通过。这是预约类系统最典型的并发翻车现场。解决:改成第三章里带容量条件的原子 UPDATE,让 book_count < capacity 参与 where 判断;同时订单表加 user_id + course_id 唯一索引。改完后用两个微信开发者工具窗口同时点预约,验证只有一个成功,另一个收到“已约满”提示。这一条在答辩时可以直接当并发优化案例讲。

5.3 现象:微信开发者工具里请求正常,真机预览白屏或请求失败

原因:开发者工具勾选了“不校验合法域名”,真机环境没有这个便利开关,真机只认已配置的 request 合法域名。解决:本地联调可以用工具里的免校验选项,答辩真机演示前把接口域名配置到小程序后台的 request 合法域名列表里。如果后端没有公网域名,就准备一台同局域网服务器,真机连同一 WiFi 后用 IP 访问,同时确认防火墙端口放行。白屏还有一个常见来源是前端 JS 报错,打开调试面板看 console,别只盯着页面。

5.4 现象:SpringBoot 启动成功,但首次访问接口报 Server returns invalid timezone,或连接超时

原因:MySQL 8.0 驱动对时区要求严格,url 没带 serverTimezone。解决:连接串改成 jdbc:mysql://localhost:3306/gym?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false。如果还报连接超时,按顺序检查 MySQL 服务是否启动、端口是否 3306、防火墙是否放行、MySQL 的 bind-address 是否允许外部连接。一个更容易被忽略的低级错误是 yml 配置里直接复制了带 & 的连接串,但 YAML 里 & 有特殊含义,需要对整段 jdbc-url 加引号。

5.5 现象:Vue 后台 npm run build 打包后接口全部 404,刷新页面空白

原因:打包时接口地址还指向开发环境 localhost;路由用了 history 模式,刷新请求直接打到服务器;或者把 dist 塞进 SpringBoot 的 static 目录后没处理资源路径。解决:打包前把接口 baseURL 改成服务器地址,Vue 用环境变量区分 dev 和 prod;路由别用 history 模式,用 hash 模式能避免刷新 404;如果确实要把 Vue 打包产物放进 SpringBoot 里打成单 jar,把 dist 内容放到 resources/static,并在后端配置前端路由的 fallback 转发。还有高频问题:打包后 index.html 里的 js/css 是绝对路径,部署到子路径下资源加载不出来,在 vue.config.js 里把 publicPath 设为 './' 即可。

这五个问题基本覆盖了我见过的毕业设计大部分翻车现场。如果逐条排查完还有问题,最有效的不是瞎改配置,而是打开浏览器 DevTools 或小程序调试面板,看接口返回的堆栈,把异常信息完整贴到搜索引擎里找答案。网络请求层面优先用开发者工具自带的 Network 面板看 request 和 response,比猜配置省时间。

6. 答辩前加三个加分项:超时关单、并发演示与一键造数

6.1 加分项一:用 @Scheduled 自动关闭超时未支付订单

如果订单状态里有“待支付”,就必须有超时释放逻辑。加一个定时任务,每 30 秒扫一次状态为 0 且创建时间早于当前时间 15 分钟的订单,改为过期状态,同时回补课程人数:

@Scheduled(fixedDelay = 30000) @Transactional public void closeExpiredOrders() { List<AppointmentOrder> list = orderMapper.selectList( new LambdaQueryWrapper<AppointmentOrder>() .eq(AppointmentOrder::getStatus, 0) .lt(AppointmentOrder::getCreateTime, LocalDateTime.now().minusMinutes(15))); for (AppointmentOrder order : list) { order.setStatus(4); // 已过期 orderMapper.updateById(order); courseMapper.update(null, new LambdaUpdateWrapper<Course>() .eq(Course::getId, order.getCourseId()) .setSql("booked_count = booked_count - 1")); } }

这段逻辑要记得在启动类上加 @EnableScheduling。更新订单和回补库存包在同一个事务里,避免中间失败导致数据不一致。演示时可以看到,15 分钟不支付的订单自动释放名额,这个细节比静态页面更有说服力。

6.2 加分项二:并发抢课演示,把超卖变成亮点

答辩现场不用真的拿 JMeter 压测,一个容量为 1 的课程、两台开发者工具窗口、两个不同账号同时点预约,只要稳定出现“一个成功、一个已约满”,就已经把并发控制讲清楚了。演示前先确认课程 booked_count 是初始值,再准备两个登录好的账号。我会提前录一份屏幕视频,防止现场网络波动影响效果,这既是备份,也是给自己留的后悔药。

6.3 加分项三:一键造数脚本,别让后台空着开始演示

准备一份 data.sql,初始化管理员账号、教练、三间场地、未来一周的课程,还有两个演示用户和各自的会员卡。数据量控制在几十条就够,别把表塞得密密麻麻。造数脚本里的手机号和昵称一定要用假的演示数据,不要用自己的真实信息,这也是基本的数据安全习惯。功能做完后跑一遍脚本重置环境,从用户打开小程序到订单完成,全程录屏自测一遍,能发现很多临场才会暴露的问题。

我自己早期做这类系统时,吃过一个亏:功能全做完了,没准备演示数据,答辩现场现录课程,页面空荡荡,导师一句“你的预约记录呢”就直接冷场。后来养成的习惯是:动手写代码前先把数据库初始化脚本和演示账号准备好,功能完成立刻用脚本重置环境跑一遍全流程。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询