ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SSM+微信小程序健身房私教预约系统设计与实现(含状态机与乐观锁)

SSM+微信小程序健身房私教预约系统设计与实现(含状态机与乐观锁) 简介预约管理系统在日常生活中无处不在从健身房私教课到医疗挂号其核心都离不开时间冲突检测与业务状态流转。如何保证多个用户同时抢约同一时段时数据不错乱如何清晰表达预约从待确认到已完成的完整生命周期这背后依赖的是状态机设计与乐观锁等工程化手段。本文基于SSM框架与微信小程序组合从数据库表结构设计出发解析时段管理、预约下单、取消释放等核心流程并深入探讨事务控制、并发防超卖、登录鉴权等关键技术细节。该方案兼顾后端业务逻辑与前端交互体验适用于校园毕设、企业级预约类应用的快速原型落地也为开发者理解SpringMVC、MyBatis与小程序生态提供了完整实践参考。 做Java毕设这几年被问到最多的就是“健身房私教预约系统”这类选题其实它的本质就是一个带业务状态的预约管理系统核心在“时间冲突检测”和“教练-学员-课程”的三角关系建模上。SSM 微信小程序这套组合之所以常青是因为它覆盖了后端ORM、前端交互、API对接三个维度难度适中答辩也有东西可讲。今天我把这套系统的完整设计思路、核心表结构、关键代码和踩坑记录一次说清楚。1. 项目整体设计与技术选型思路1.1 为什么是SSM 微信小程序而不是Spring Boot Vue先说结论如果你是为了快速过毕设且要兼顾文档完整性SSM其实比Spring Boot更“好写”。不是因为Spring Boot不好而是SSM的面试和答辩点非常固定——Spring的IOC/AOP、SpringMVC的请求流转、MyBatis的SQL掌控力这三个点你可以在文档里写得非常细导师问起来你也有话说。微信小程序端选原生而不是Uniapp理由是原生小程序对组件的调用、生命周期、分包加载的掌握度更直观而且毕设文档里可以少写一层“框架封装”的复杂度。你只需要管好四个文件.wxml结构、.wxss样式、.js逻辑、.json配置比Uniapp的Vue语法 编译踩坑要省心得多。这套系统的业务边界很清晰用户端微信小程序注册登录、浏览教练、查看课程、发起预约、取消预约、查看我的课表。管理端Web页面用JSP或Layui教练管理、课程管理、预约审核、会员管理、数据概览。我见过很多人把管理端做成第二个小程序这是没必要的。管理端用浏览器访问更符合健身房前台的操作习惯这也是SSM JSP的老本行。1.2 预约系统的核心难点不在CRUD而在“状态流”很多同学拿到这个题目就开始写增删改查写到预约表的时候发现不对劲预约不是insert一下就完事的。一个合格的预约单至少要有这些状态待确认用户提交预约等待教练/管理员确认。已确认教练同意带课课程锁定。已完成课程时间已过用户签到或自动完成。已取消用户或教练取消课时释放。已爽约预约确认后未到课记录违约次数。这个状态流就是你的核心业务逻辑BATJ面试题库里的“订单状态机”其实就是这么回事。你在文档里画一张状态流转图再配合代码里的状态判断导师会认为你有工程意识。2. 数据库设计与核心表结构拆解2.1 六张核心表一张都不能少我按实际开发顺序推荐大家这样建表第一张用户表user字段必须有openid、nickname、avatar、phone、role区分会员和教练、status。openid是微信小程序用户的唯一标识千万不能漏。第二张教练表coach如果教练也是平台用户可以直接复用user表的role字段另外建一张coach_info表存教龄、专长、评分、简介。我建议分两张表因为教练的审核状态和管理员的审核状态单独管理会简单很多。第三张课程表course字段包含course_name、course_type私教课/团课/康复课、duration课时长、price、coach_id关联教练、max_students约满人数。第四张时段表time_slot这个表非常容易被忽略。预约系统的核心不是“课程”而是“某天某个教练的某个时段”。时段表的字段coach_id、slot_date、start_time、end_time、is_booked、version乐观锁用。第五张预约表appointment也就是订单表。字段包含appointment_no订单号、user_id、coach_id、course_id、slot_id关联到具体时段、status、create_time、cancel_reason。第六张评价表review字段包含appointment_id、rating、content、create_time。评价表会让你的系统在功能展示上多一个亮点而且实现起来并不复杂。2.2 为什么时段表要单独拆出来而不是直接存预约时间这是个经验问题。如果你把“预约时间”直接存在appointment表里那当用户取消预约或者教练调整课程时你需要把所有相关记录查出来再逐一修改非常痛苦。拆出时段表之后你的业务逻辑变成这样教练上课 锁定一个时段is_booked 1。用户预约 在某个未被锁定的时段上创建预约单。取消预约 释放时段is_booked 0同时把预约单状态改为已取消。这种设计还有一个额外的好处你可以非常容易地查出“某个教练本周的可用时段”小程序端直接select * from time_slot where coach_id ? and is_booked 0前端渲染成时间选择器用户体验很流畅。我在实际项目中用到的核心建表SQL片段简化版如下CREATE TABLE time_slot ( id int(11) NOT NULL AUTO_INCREMENT, coach_id int(11) NOT NULL COMMENT 教练ID, slot_date date NOT NULL COMMENT 日期, start_time varchar(10) NOT NULL COMMENT 开始时间 HH:mm, end_time varchar(10) NOT NULL COMMENT 结束时间 HH:mm, is_booked tinyint(1) DEFAULT 0 COMMENT 0可约 1已约, version int(11) DEFAULT 0 COMMENT 乐观锁版本号, PRIMARY KEY (id), KEY idx_date_coach (slot_date, coach_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意这里加了version字段就是为了防止多个用户同时抢同一个时段造成超卖。后面代码部分我会专门讲乐观锁怎么写。3. 后端核心功能实现从Controller到Mapper的完整链路3.1 SpringMVC三层结构怎么安排SSM项目的包结构建议这样分controller接收前端请求只做参数接收和结果封装。service业务逻辑包括事务控制、状态判断、时间冲突检测。dao/mapperMyBatis对外接口只写SQL。pojo/entity实体类。common统一返回结果、异常处理、工具类。Controller层的代码风格保持一致很重要我习惯统一返回一个Result对象public class ResultT { private Integer code; // 200成功 500失败 private String message; private T data; }这样小程序端wx.request拿到响应后直接判断code是否为200再决定是否展示数据避免前端处理各种奇奇怪怪的返回结构。Controller示例RestController RequestMapping(/api/appointment) public class AppointmentController { Resource private AppointmentService appointmentService; PostMapping(/create) public Result create(RequestBody AppointmentCreateDTO dto) { return appointmentService.createAppointment(dto); } PostMapping(/cancel) public Result cancel(RequestParam Long appointmentId, RequestParam String openid) { return appointmentService.cancelAppointment(appointmentId, openid); } GetMapping(/myList) public Result myList(RequestParam String openid, RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize) { return appointmentService.getUserAppointments(openid, pageNum, pageSize); } }3.2 预约下单的核心Service逻辑事务 乐观锁这是整个系统最值得拿出来讲的部分。预约动作发生在两个表上time_slot要改成已约状态appointment要插入一条记录。这两步必须在一个事务里否则会出现“时段锁了但没有订单”或者“订单创建成功但时段还是可约”的脏数据。我给出的实现方案第一步查询时段校验状态。第二步用乐观锁更新时段update ... where id ? and is_booked 0。第三步插入预约单。第四步返回预约结果。注意第二步必须判断更新影响的行数如果影响行数为0说明这个时段刚刚被别人抢了直接抛出业务异常。Override Transactional(rollbackFor Exception.class) public Result createAppointment(AppointmentCreateDTO dto) { // 1. 校验时段存在且未被预约 TimeSlot slot timeSlotMapper.selectById(dto.getSlotId()); if (slot null) { return Result.error(时段不存在); } if (slot.getIsBooked() 1) { return Result.error(该时段已被预约请选择其他时段); } // 2. 乐观锁尝试锁定时段 int rows timeSlotMapper.lockSlot(dto.getSlotId(), slot.getVersion()); if (rows 0) { return Result.error(手慢了该时段刚刚被约走了); } // 3. 生成预约单 Appointment appointment new Appointment(); appointment.setAppointmentNo(generateNo()); // 时间戳随机数 appointment.setUserId(dto.getUserId()); appointment.setCoachId(slot.getCoachId()); appointment.setCourseId(dto.getCourseId()); appointment.setSlotId(slot.getId()); appointment.setStatus(0); // 待确认 appointment.setCreateTime(new Date()); appointmentMapper.insert(appointment); // 4. 新增待办消息可选通知教练 // messageService.sendToCoach(slot.getCoachId(), 您有新的预约申请); return Result.success(appointment.getId()); }对应的MyBatis XMLupdate idlockSlot UPDATE time_slot SET is_booked 1, version version 1 WHERE id #{id} AND is_booked 0 AND version #{version} /update这套写法的核心就是“先更新后插入”在并发量不高的毕设场景下是绝对够用的。如果在文档里想体现更进一步的水平可以在第三步插入预约单之前再查一次教练当天是否存在时间重叠的预约做一个业务层的二次校验。这部分我放在常见问题里展开。3.3 取消预约的状态机和课时释放取消预约相对容易但状态判断不能省。用户只能取消“待确认”和“已确认”的预约已完成和已取消的单子不能取消。我建议的流程根据appointmentId查出订单校验openid归属。判断status是否为0或1不是则返回“当前状态不可取消”。修改订单状态为2已取消。把对应的time_slot释放is_booked改回0。这里有个细节释放时段的时候不一定需要乐观锁因为此时时段大概率是锁定状态而且只有预约人自己才能取消自己的单子。不过为了安全起见释放SQL的WHERE条件里加上is_booked 1更稳妥。Override Transactional(rollbackFor Exception.class) public Result cancelAppointment(Long appointmentId, String openid) { Appointment appointment appointmentMapper.selectById(appointmentId); if (appointment null) { return Result.error(预约单不存在); } // 校验归属假设user表有openid关联 if (!appointment.getOpenid().equals(openid)) { return Result.error(无权操作该预约); } if (appointment.getStatus() ! 0 appointment.getStatus() ! 1) { return Result.error(当前状态不可取消); } appointment.setStatus(2); appointment.setCancelReason(用户主动取消); appointmentMapper.updateById(appointment); timeSlotMapper.releaseSlot(appointment.getSlotId()); return Result.success(); }写到这里可能有人问为什么不在取消预约时直接删除time_slot记录因为保留时段记录是必要的这样可以统计教练的排班历史、被取消的次数甚至可以分析哪些时段最容易被取消帮助健身房优化排课。删除是不可逆操作不做。4. 微信小程序端实现从登录到预约的完整交互4.1 小程序登录态与后端会话校验小程序端最容易被忽视的是登录态的建立。很多同学写小程序只关心页面跳转忽略了后端如何识别“当前这个请求来自哪个用户”。微信小程序的标准登录流程小程序端调用wx.login()获取临时code。把code传给后端。后端拿code appid secret去微信接口换取openid。后端用openid在user表查找用户存在则返回登录成功不存在则自动注册。后端返回自定义登录态我习惯用UUID或JWT小程序存入storage。后续请求带Authorization: token后端用拦截器解析。这个流程不要省。如果你只是把openid存在小程序端然后每次请求都传openid那别人抓包就能伪造成任意用户答辩时这段会被导师质疑。建议SSM里使用一个简单的HandlerInterceptor做登录校验public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { response.setStatus(401); return false; } // 解析token获取userId并存入ThreadLocal Long userId JwtUtil.parseToken(token); if (userId null) { response.setStatus(401); return false; } UserContext.setUserId(userId); return true; } }4.2 首页-教练列表-预约下单的页面逻辑小程序端我建议三个核心页面index首页展示推荐课程、热门教练。coachList教练列表支持按课程类型筛选。booking预约页选择教练、日期、时段确认下单。预约页的难点在于“时段组件的状态展示”。我的做法是后端提供一个接口GET /api/coach/slots?coachId1date2025-01-10返回当日所有时段及状态前端遍历渲染成时间块。小程序端核心代码Page({ data: { coachId: null, date: , slots: [], // [{id: 1, startTime: 10:00, endTime: 11:00, isBooked: 0}] selectedSlotId: null }, onLoad(options) { this.setData({ coachId: options.coachId }) this.loadSlots() }, loadSlots() { wx.request({ url: https://yourdomain.com/api/coach/slots, method: GET, data: { coachId: this.data.coachId, date: this.data.date }, header: { Authorization: wx.getStorageSync(token) }, success: (res) { if (res.data.code 200) { this.setData({ slots: res.data.data }) } } }) }, selectSlot(e) { // 已经约满不可选 const slot e.currentTarget.dataset.slot if (slot.isBooked 1) { wx.showToast({ title: 该时段已被约, icon: none }) return } this.setData({ selectedSlotId: slot.id }) }, submitBooking() { if (!this.data.selectedSlotId) { wx.showToast({ title: 请选择时段, icon: none }) return } wx.request({ url: https://yourdomain.com/api/appointment/create, method: POST, data: { slotId: this.data.selectedSlotId, courseId: this.data.courseId, userId: this.data.userId }, header: { Authorization: wx.getStorageSync(token) }, success: (res) { if (res.data.code 200) { wx.showToast({ title: 预约成功 }) wx.navigateTo({ url: /pages/myOrder/myOrder }) } else { wx.showModal({ title: 预约失败, content: res.data.message, showCancel: false }) // 刷新时段把被别人抢走的时段标记出来 this.loadSlots() } } }) } })这段代码里有一个容易被忽略的细节预约失败后一定要重新加载时段列表。否则用户看到的数据是旧的可能会以为系统出bug了。这种交互上的小细节建议写进文档的“界面优化”章节是加分项。4.3 管理端Web页面的模块划分管理端用SSM JSP或者SSM Layui都可以。我建议用Layui因为它的表格组件和表单组件能节省大量前端时间而且Layui的文档在中文圈里非常友好。管理端最少要含五个模块仪表盘统计今日预约数、总会员数、课程总数。教练管理新增/编辑/删除教练配置教练可预约时段。课程管理维护课程类型、价格、时长。预约管理查看所有预约支持按状态筛选、确认/取消预约。会员管理查看用户列表禁用/启用账号。其中“教练可预约时段”的配置功能是你系统的亮点因为很多基础毕设只是预置一批死数据而你这个功能允许管理员动态为教练添加可约时段会增加系统的完整度和灵活性。5. 常见问题与排查技巧实录5.1 “明明取消了预约为什么时段还是不可选”这个问题的原因通常是两种第一种取消预约接口只更新了appointment表没有释放time_slot。第二种释放时段的SQL条件写错比如where id ?写成了where slot_id ?。排查方法很简单打开数据库直接查两条记录看appointment表的status是否已变为2。看time_slot表的is_booked是否已变为0。如果appointment变了而time_slot没变问题一定出在Service层的释放逻辑。我建议在Service里把释放时段的操作放在finally块或者事务内靠后的位置保证它一定会执行。5.2 “两个用户同时抢同一个时段会不会超卖”如果按照我上面写的乐观锁方案是不会超卖的。但有一个隐藏坑有些同学在事务里先查询再更新按理说也是可以防住的但前提是必须使用SELECT ... FOR UPDATE行锁否则两个事务同时读到is_booked0就会重复预约。如果你用的是MyBatis实现行锁的姿势是select idselectSlotForUpdate resultTypeTimeSlot SELECT * FROM time_slot WHERE id #{id} FOR UPDATE /select然后在Service里先调用这个方法再走更新逻辑。这样事务A查到记录并锁住事务B必须等A提交或回滚之后才能查询。不过要注意FOR UPDATE必须包裹在事务里才有效否则锁会自动释放。两种方案乐观锁和悲观锁任选其一即可我的建议是毕设用乐观锁代码简洁、好讲解而且面试官问起来你能说出两种方案的优劣对比已经超过很多人了。5.3 “小程序端请求后端接口一直提示域名不合法”这是微信开发者工具里最经典的问题。小程序生产环境要求所有请求域名必须为HTTPS而且要在微信公众平台配置合法域名。本地调试阶段可以这样处理在微信开发者工具右上角“详情” - “本地设置”里勾选“不校验合法域名、web-view业务域名、TLS版本以及HTTPS证书”。这样你的后端用HTTP localhost或局域网IP也能调试。但要注意这只是本地调试上线前一定要换成HTTPS的正式域名否则真机预览会直接失败。另外如果你的后端部署在云服务器上建议用Nginx做反向代理把80端口转发到8080同时配置SSL证书。这个内容你在教程里简单提一下能帮用户规避最少半天的踩坑时间。5.4 “微信小程序端能直接访问MySQL数据库吗”绝对不能。小程序端运行在用户的微信客户端里它不可能直连你的数据库。所有数据访问必须通过后端API完成。有同学会想“那我把数据库连接信息写在小程序代码里行不行”答案是不行而且这会带来严重的安全问题——任何人反编译你的小程序包都能看到数据库地址、账号、密码。正统做法就是后端SSM提供RESTful API小程序只发HTTP请求。这套架构说白了就是一次“前后端分离”的小型实践。前端只负责展示和交互后端只负责业务逻辑和数据存储中间通过JSON格式进行通信。这个设计思想在文档里一定单独写一节叫“系统架构设计”放一张架构图用Visio或ProcessOn画包括用户、小程序、服务器、数据库四层。5.5 分页查询和模糊搜索的加分实现很多毕设只做了列表展示没做分页和搜索。导师一眼看过去就觉得功能不全。预约管理列表至少要有三个筛选条件按状态筛选、按教练名搜索、按日期范围搜索。Controller参数这样设计GetMapping(/admin/list) public Result page(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) Integer status, RequestParam(required false) String coachName, RequestParam(required false) String beginDate, RequestParam(required false) String endDate) { return appointmentService.pageQuery(pageNum, pageSize, status, coachName, beginDate, endDate); }Service里用MyBatis的where标签动态拼接条件既安全又简洁select idpageQuery resultTypeAppointmentVO SELECT a.*, c.name AS coachName, u.nickname AS userName FROM appointment a LEFT JOIN coach c ON a.coach_id c.id LEFT JOIN user u ON a.user_id u.id where if teststatus ! null AND a.status #{status} /if if testcoachName ! null and coachName ! AND c.name LIKE CONCAT(%, #{coachName}, %) /if if testbeginDate ! null and beginDate ! AND a.create_time gt; #{beginDate} /if if testendDate ! null and endDate ! AND a.create_time lt; #{endDate} /if /where ORDER BY a.create_time DESC LIMIT #{offset}, #{pageSize} /select注意这里用LEFT JOIN关联查询教练名和用户名这样前端表格可以直接展示文字而不是外键ID人工审核预约单的时候体验会好很多。6. 文档编写与答辩准备的实战建议6.1 毕业设计文档的章节布局含文档是这个项目的标配很多人以为文档是最后写我的建议是文档边写代码边穿插写。用四天的时间安排来说第一天搭数据库 建工程 配置SSM同步编写第三章“需求分析”和第四章“数据库设计”。第二天完成用户登录、教练列表和预约下单功能同步写第五章“系统实现”的前半部分。第三天完成管理端和取消预约功能补充“系统测试”章节。第四天整理“摘要”、“绪论”、“总结与展望”美化UML图和截图。这样做的好处是代码跑通了文档细节顺手就写不会出现代码和文档不一致的尴尬。重点的UML图要求用例图管理员、教练、用户三个角色各一张或合并成一张大图。类图核心实体类和Service接口的UML类图。时序图用户预约下单的时序图从用户点击到数据库提交。E-R图六张表的实体关系图。这些图用PowerDesigner、Visio或者StarUML都能画不建议在Word里手绘太痛苦了而且画出来改起来也麻烦。6.2 答辩可能会被追问的五个问题导师们虽然不指望你做出工业级产品但基本逻辑必须自洽。我整理了这五个高频问题第一个问题“为什么用SSM而不用Spring Boot” 正确思路不是不会用Spring Boot而是SSM的配置过程更透明可以更清晰地展示Spring核心思想比如IOC容器如何管理Bean、SpringMVC的DispatcherServlet如何分发请求、MyBatis如何通过动态代理生成Mapper实现类。第二个问题“如何防止同一个时段被多人同时预约” 答乐观锁方案更新时带上version影响行数为0就提示失败。顺便补充悲观锁的对比说明乐观锁更适合读多写少的场景。第三个问题“预约完成后如何通知用户或教练” 如果系统里做了消息表或者利用小程序订阅消息就如实说如果没做可以说“目前设计中预留了消息通知接口后续可通过微信订阅消息实现”。诚实且给出改进方向比吹牛强。第四个问题“如果教练临时请假预约单如何处理” 可以答管理员在管理端取消该时段及相关预约单系统自动释放时段同时用户端可看到预约状态变为取消并提示重新预约。这个逻辑要提前在代码里实现好答复案更扎实。第五个问题“系统中哪些功能用了事务” 至少要说预约下单更新时段插入预约单、取消预约更新订单释放时段。顺带解释为什么这两个场景必须加Transactional。6.3 视频教程里可以额外展开的内容含教程是这个项目的加分项。我建议教程视频不要超过5个每个控制在15-20分钟分别讲视频1环境搭建JDK、Maven、Tomcat、MySQL、微信开发者工具。视频2数据库导入和SSM框架启动。视频3后端接口调试Postman调用登录、教练列表、预约接口。视频4小程序端功能演示与接口联调。视频5常见问题修复502、404、跨域、端口占用。每个视频开头先把最终效果演示一遍再进入操作。观众能看到希望才有动力跟着做这也是我做教程一年多总结出来的经验。7. 部署上线与后续扩展方向7.1 从本地到服务器的部署清单如果你有云服务器建议部署一遍哪怕只是为了答辩时秀一下“真机线上访问”。部署步骤没有特别复杂安装JDK 8 Tomcat 8.5 MySQL 5.7或云数据库RDS。打包项目用IDEA的Maven插件执行clean package打war包丢到Tomcat的webapps目录。导入数据库脚本修改jdbc.properties中的数据库连接信息。配置Nginx反向代理和SSL证书把443端口代理到8080。小程序后台配置合法域名request合法域名填你的HTTPS域名。Tomcat下运行SSM项目要注意一个点如果你的JSP放在webapp目录可能因为Servlet版本不兼容导致404。建议打包前先确认pom.xml里的javax.servlet-api版本是3.1.0对应Tomcat 8.5。7.2 如果想继续拔高哪些功能值得加如果你的进度比较快或者想拿个高分可以往这几个方向扩展教练端小程序让教练用小程序的“工作台”接单和取消而非登录Web管理端。本质上复用已有的API只需要新增一个带“教练角色”判断的小程序端页面。微信订阅消息通知预约成功和取消时推送订阅消息给用户需要在微信公众平台申请模板前端用wx.requestSubscribeMessage授权。基于Redis的时段缓存将未来7天的时段列表缓存到Redis减少数据库压力。毕设能提到Redis已经是亮点不要求你真正做得很完善。数据可视化面板管理端用ECharts展示每周预约趋势、热门教练排名视觉效果非常炸裂。这些扩展不用全部实现选一两个做即可关键是文档里把它们写成“系统特色”再配合截图导师的观感会完全不同。7.3 我的最终建议做毕设最忌讳的是贪多求全。一个私教预约系统能把这六张表设计清楚、预约和取消这两个核心流程完整跑通、管理端能正常维护数据、文档能画出完整的UML图这已经能评优秀了。剩下的时间宁可用来多刷几套微信小程序和SSM的面试题也不要盲目堆砌功能。如果你在做的过程中还有具体问题记住先查日志。SSM项目定位问题最有效的方式就是看Tomcat的catalina.out日志找到报错堆栈的第一行Google也好百度也好把报错信息原样复制进去绝大多数问题都能找到答案。自己动手排错的那几分钟比看十遍教程都管用。本文还有配套的精品资源点击获取
返回列表