☰
微信小程序+Java后端琴房管理系统毕业设计实现指南
2026/10/9 6:17:50 网站建设 项目流程

简介:基于微信小程序与Java后端构建的琴房管理系统,是一套面向毕业设计、课程设计及项目实战的完整工程,覆盖学生端在线预约、琴房信息浏览、留言提交,以及管理员端的轮播公告、琴房类型与预约审核管理,形成前后台联动的业务闭环。资源包共包含1098个文件,压缩后约18.76MB,其中png/jpg图片用于界面与素材,vue文件承载后台管理页面,java类实现核心业务与数据访问,js、wxml等构成小程序侧逻辑,sql脚本提供数据库初始化数据,另有说明文档和演示视频辅助上手,整体目录划分明确,便于按功能模块查阅。该资料已有175人学习下载,适合需要快速获取可运行参考系统的在校生或初级开发者。内容提供完整源码、数据库脚本、配套说明与实操演示视频,导入开发工具即可运行,也可重点学习其小程序与Java后台的接口设计、登录鉴权、预约状态流转等实现方式,为独立完成同类毕业设计提供直接范本。

1. 琴房管理系统毕业设计:为什么这个题目值得做、适合谁

每年毕设季,“基于微信小程序+java后端的琴房管理系统”都是出现频率很高的题目。这套系统的本质是一个带实时预约的资源管理场景:学生在小程序里查空闲琴房、发起预约,管理员在后台审核、排课,Java 后端负责业务规则和权限,MySQL 负责持久化。相比纯网页管理系统,它多出小程序端适配与微信登录联调,整体工作量更饱满,答辨证也更有的聊。适合两类人:一类是时间紧,想要一个能快速跑通、能录演示视频的完整项目;另一类是真心想搞清楚前后端分离项目实战是怎么落地的同学。别急着解压 rar,先想清楚系统怎么拆,后面才不会翻车。

2. 先定架构再写代码:小程序 + Java 后端的系统边界与技术选型

拿到题目后最容易犯的错是直接开写。小程序端写一堆业务判断,后端只做增删改查,结果一到并发和状态流转就露馅。所以第一步是确定系统边界:小程序负责什么、后端负责什么、接口长什么样。

2.1 小程序端与后端各自的职责边界

小程序端只做三件事:渲染页面、收集用户输入、调用后端接口。它可以把日期选择器、时段按钮、表单校验做得很丰富,但不要在里面判断“这个时段是否已被预约”。小程序代码一旦发布就能被反编译,把业务规则放在前端等于把底牌亮给外人,而且同一套规则无法被管理员端复用。真正该做的是把规则收拢到后端。

后端要管的是鉴权、预约冲突检测、状态流转、数据持久化。以预约为例,用户提交一个“周三 14:00-15:00 预约 3 号琴房”,后端要判断这个琴房是否存在、是否在营业时间、该时段是否已有通过审核或正在使用中的预约,然后才决定插入一条新记录。至于管理员端,常见做法是同一套小程序里根据角色显示不同页面,也可以单独做一个极简的网页管理后台。毕业设计用前者更省事,因为不用额外部署一套前端工程。

这里可以参考一个和产品经理对齐需求时的思路:把自己当后端,把老师当产品经理,先把状态机画出来——预约从“待审批”到“已通过”“已签到”“已结束”谁能操作、什么条件触发。边界理清了,后端的接口列表基本就出来了。

2.2 技术栈选型:Spring Boot + MyBatis Plus + MySQL 的搭配理由

Java 后端最常见的组合是 Spring Boot + MyBatis Plus + MySQL,源码包里大概率也是这套。Spring Boot 省掉大量 XML 配置,内嵌 Tomcat,打成一个 jar 就能跑。MyBatis Plus 在 MyBatis 之上封装了单表 CRUD,写接口时只需要继承ServiceImpl,大部分数据库增删改查不用手写 SQL,能省不少时间。MySQL 则是学生最熟悉的数据库,建表、导入、导出都方便,遇到问题也容易查。

为什么不用原生 MyBatis?不是不能用,而是原生 MyBatis 需要为每个实体维护 Mapper XML,字段一多就很繁琐。但要注意,MyBatis Plus 只是简化单表,联表复杂查询还是要自己写 SQL,不要硬套。

前端选型上,题目明确是“微信小程序”,那就直接用小程序原生开发,不要上来就套 uniapp。原生组件对微信 API 的兼容性最好,包体也小。如果后面想多端复用再考虑 uniapp,但 uniapp 打包成微信小程序时,自定义导航栏和组件库可能出现适配差异,属于额外的坑。

提示:Spring Boot 版本建议选 2.7.x 配 JDK 1.8,稳定且网上资料多。Spring Boot 3.x 强制 JDK 17,翻车概率高,除非老师有要求,否则没必要在毕设阶段折腾。

2.3 前后端分离的接口约定:统一返回结构与 RESTful 风格

前后端分离项目实战里,最常见的接口问题是各接口返回格式不一致。有的返回{status:1},有的返回{code:200},小程序端就要到处 if 判断,非常痛苦。我一般会先写一个统一返回体,后端所有接口都套这个壳。

public class Result<T> { private Integer code; // 200 成功,401 未登录,403 无权限,500 业务异常 private String message; // 提示信息 private T data; // 真正的数据 public static <T> Result<T> ok(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("ok"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }

这段代码的用意是让前后端只认一种格式。前端wx.request回调里拿到res.data.code,为 200 就取数据,否则弹出message。分页接口再包一层PageResult,把 total 和 records 拆开,方便小程序端做触底加载。

接口风格上使用 RESTful,按资源划分,常见接口可以这样设计:

方法路径作用
POST/api/auth/login微信登录或账号密码登录
GET/api/rooms琴房列表(可按状态过滤)
POST/api/reservations创建预约
GET/api/reservations/my我的预约
PUT/api/reservations/{id}/approve管理员审批
PUT/api/reservations/{id}/cancel取消预约

统一返回结构的意义不只在于好看,还在于后端异常能统一转成 JSON,而不是抛一堆堆栈给小程序。把接口约定做在前面,后面联调时能少掉 80% 的沟通成本。

3. 把琴房业务落到数据库:核心表设计、字段约束与建表 SQL

数据库是后端的信任基础。表结构设计得好,后面 CRUD 都是一路绿灯;设计得差,后期补字段、对时间、改状态会让你怀疑人生。

3.1 五张核心表与字段清单

一套完整的琴房管理系统至少需要用户表、琴房表、预约表、设备表、公告表。我常用的表清单如下:

表名说明关键字段
sys_user用户表id, username, password, phone, openid, role, nickname
music_room琴房表id, name, location, capacity, start_time, end_time, status
room_reservation预约表id, user_id, room_id, reserve_date, start_time, end_time, purpose, status
room_device设备表id, room_id, device_name, device_status
sys_notice公告表id, title, content, create_time

不要用user作为表名,MySQL 里user是保留字,后面写 SQL 会加反引号,纯粹给自己找麻烦。用户表里存openid是为了对接小程序登录;phone字段如果毕设拿不到真实手机号,可以先允许为空或存模拟手机号。角色字段role用字符串STUDENT/ADMIN,比用数字可读性好,答辩时也容易解释。

琴房表的start_time和end_time保存的是每天开放时段的起止时间,比如 08:00 到 22:00,注意这是时间,不是日期。设备表关联琴房,用来做设备报修或状态展示,属于加分项,不做也不影响主流程。公告表则用于在小程序首页滚动展示通知,能增加系统完整度。

3.2 预约状态机与时间冲突约束

预约表是整个系统的重中之重,核心是状态机设计。我一般用这些状态:PENDING(待审批)、APPROVED(已通过)、REJECTED(已拒绝)、CHECK_IN(已签到)、CHECK_OUT(已结束)、CANCELLED(已取消)。学生提交预约后进入待审批,管理员审批通过后变为已通过,学生到琴房扫码签到后变为已签到,练完琴后手动或由管理员设为已结束。

为什么要做状态机?因为同一个琴房同一段时间只能有一种“占用”状态。如果不区分状态,所有记录混在一起,冲突查询就会把取消的记录也算进去,最终导致明明已经释放了时段,用户却还是约不上。

冲突判断是预约系统最核心的逻辑,用时间段重叠公式:一条预约与现有占用记录冲突,当且仅当新预约开始时间小于旧预约结束时间,且新预约结束时间大于旧预约开始时间。对应 SQL 如下:

SELECT COUNT(*) FROM room_reservation WHERE room_id = #{roomId} AND reserve_date = #{date} AND status IN ('APPROVED', 'CHECK_IN') AND start_time < #{endTime} AND end_time > #{startTime};

这里start_time < endTime AND end_time > startTime是开区间判断,可以正确处理紧邻时段。比如 10:00-11:00 和 11:00-12:00,前者 end_time 为 11:00,后者 start_time 为 11:00,11:00 < 11:00为假,所以不冲突,符合直觉。这个公式建议直接写在后端 Service 里,不要让前端算。

3.3 建表 SQL 与初始化数据

下面给出一份可直接执行的 MySQL 建表脚本。

CREATE DATABASE IF NOT EXISTS qinfang DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE qinfang; CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(100) NOT NULL COMMENT 'BCrypt加密后的密码', phone VARCHAR(20) COMMENT '手机号', openid VARCHAR(64) COMMENT '微信openid', nickname VARCHAR(50) COMMENT '昵称', role VARCHAR(20) NOT NULL DEFAULT 'STUDENT' COMMENT 'STUDENT/ADMIN', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, deleted TINYINT DEFAULT 0 COMMENT '逻辑删除' ) ENGINE=InnoDB COMMENT '用户表'; CREATE TABLE music_room ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT '琴房名称', location VARCHAR(100) COMMENT '所在位置', capacity INT DEFAULT 4 COMMENT '可容纳人数', start_time TIME NOT NULL DEFAULT '08:00:00' COMMENT '开放开始时间', end_time TIME NOT NULL DEFAULT '22:00:00' COMMENT '开放结束时间', status TINYINT DEFAULT 1 COMMENT '1可用 0停用', deleted TINYINT DEFAULT 0 ) ENGINE=InnoDB COMMENT '琴房表'; CREATE TABLE room_reservation ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, room_id BIGINT NOT NULL, reserve_date DATE NOT NULL COMMENT '预约日期', start_time TIME NOT NULL COMMENT '预约开始时间', end_time TIME NOT NULL COMMENT '预约结束时间', purpose VARCHAR(200) COMMENT '用途', status VARCHAR(20) NOT NULL DEFAULT 'PENDING', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, audit_time DATETIME COMMENT '审批时间', deleted TINYINT DEFAULT 0, KEY idx_room_date (room_id, reserve_date) ) ENGINE=InnoDB COMMENT '预约表';

注意几个细节:字符集统一用utf8mb4,不然小程序端输入 emoji 或生僻字会入库报错;时间字段用TIME而不是VARCHAR,这样才能在 SQL 里比较大小;room_reservation建了复合索引(room_id, reserve_date),因为冲突查询都是按琴房和日期先过滤。

初始化数据至少要有一个管理员和一个可用琴房。管理员密码不能存明文,要用 BCrypt 加密后入库。你可以用 Spring Security 自带工具生成哈希,也可以在启动时通过CommandLineRunner初始化。下面是一条示例数据:

INSERT INTO sys_user (username, password, nickname, role) VALUES ('admin', '$2a$10$7QEtT0M0vG9kYpZ2K8uX3.xxxxxx', '管理员', 'ADMIN'); INSERT INTO music_room (name, location, start_time, end_time) VALUES ('A101', '艺术楼一层', '08:00:00', '22:00:00');

有同学问“MyBatis Plus 能不能根据 Java 实体类生成建表 SQL”,这里说清楚:MyBatis Plus 本身不负责建表,它是在表已经存在的基础上生成实体类和 Mapper。正确做法是先执行上面这份 SQL,再用 MyBatis Plus 代码生成器反向生成 Java 代码。实体类上加上@TableName("sys_user")、逻辑删除字段加@TableLogic,字段映射就对齐了。

4. Java 后端实现:登录鉴权、预约并发与状态流转

后端代码的组织方式我习惯按模块拆:controller 只接收参数和返回结果,service 写业务逻辑,mapper 负责数据库操作。这样写的好处是答辩时被问到“业务逻辑在哪一层”,可以直接回答在 service。

4.1 小程序登录与 JWT 鉴权的最小实现

小程序登录的标准流程是wx.login拿到临时code,后端拿code去微信接口换openid,然后根据openid查用户,存在则登录,不存在则自动注册,最后签发一个 token 返回给小程序。后端核心逻辑如下:

public String wxLogin(String code) { // 1. 调微信接口用 code 换取 openid,code 五分钟内有效且只能使用一次 String openid = wechatService.code2Session(code); if (openid == null) { throw new BizException("登录失败,请重试"); } // 2. 根据 openid 查用户 LambdaQueryWrapper<SysUser> query = new LambdaQueryWrapper<>(); query.eq(SysUser::getOpenid, openid); SysUser user = userMapper.selectOne(query); // 3. 不存在则自动注册,默认学生角色 if (user == null) { user = new SysUser(); user.setOpenid(openid); user.setNickname("微信用户"); user.setRole("STUDENT"); userMapper.insert(user); } // 4. 生成 JWT,token 中只放 userId 和 role return JwtUtil.createToken(user.getId(), user.getRole()); }

参数说明:code是一次性的,后端每次都必须用新的code去换openid,不能缓存;openid是微信用户的唯一标识,但不应该被小程度拿到,所以 token 里只放用户主键和角色。JwtUtil.createToken内部生成一个带过期时间的 JWT,我一般设置 7 天,小程序端需要重新登录时再让用户点一次微信登录。

光有 token 还不够,还需要一个拦截器统一校验。拦截器从请求头Authorization中取出Bearer token,解析出userId和role,放到ThreadLocal里供后续业务使用。放行/api/auth/login,其余接口全部拦截。

4.2 预约接口:防止重复预约的并发控制

预约接口是整个项目里最值得拿出来讲的地方。场景是:两个学生同时看到 A101 琴房周三 14:00-15:00 空闲,同时提交预约。如果程序只是先查一遍数量、再插入一条记录,两条请求可能都通过了“查询”,然后各自插入成功,导致琴房超卖。

常见做法是使用数据库悲观锁,在事务内锁住琴房行,让同一琴房的预约请求串行处理。代码示意如下:

@Transactional public Long createReservation(CreateReservationRequest req) { // 1. 锁琴房行,串行化同一琴房的预约处理 MusicRoom room = roomMapper.selectByIdForUpdate(req.getRoomId()); if (room == null || room.getStatus() == 0) { throw new BizException("琴房不存在或已停用"); } // 2. 校验预约时间在琴房开放时段内 if (req.getStartTime().isBefore(room.getStartTime()) || req.getEndTime().isAfter(room.getEndTime())) { throw new BizException("预约时间不在琴房开放时段内"); } // 3. 查重叠的已占用预约 Long conflict = reservationMapper.countConflict( req.getRoomId(), req.getReserveDate(), req.getStartTime(), req.getEndTime()); if (conflict > 0) { throw new BizException("该时段已被预约"); } // 4. 插入预约记录,初始状态 PENDING RoomReservation reservation = new RoomReservation(); reservation.setUserId(req.getUserId()); reservation.setRoomId(req.getRoomId()); reservation.setReserveDate(req.getReserveDate()); reservation.setStartTime(req.getStartTime()); reservation.setEndTime(req.getEndTime()); reservation.setPurpose(req.getPurpose()); reservationMapper.insert(reservation); return reservation.getId(); }

selectByIdForUpdate是对music_room表主键对应行加排他锁,另一个事务想锁同一行时会被阻塞,直到前一个事务提交或回滚。因为锁的是琴房行而不是预约表,所以“同一琴房”的预约操作变成了串行,而不同琴房的预约互不影响,这也符合业务实际。

这里有两个坑:第一,事务必须走同一个数据源,锁才会生效;第二,不要在一个事务里做耗时的远程调用,比如给用户发短信、调第三方接口,否则锁的持有时间会拖得很长。毕业设计里这个方案足够稳定,答辩时也能清楚地讲出“悲观锁”和“串行化”。

<select id="countConflict" resultType="java.lang.Long"> SELECT COUNT(*) FROM room_reservation WHERE room_id = #{roomId} AND reserve_date = #{date} AND status IN ('APPROVED', 'CHECK_IN') AND start_time &lt; #{endTime} AND end_time &gt; #{startTime} AND deleted = 0 </select>

注意 XML 里小于号&lt;要转义;条件status IN ('APPROVED', 'CHECK_IN')排除了PENDING、REJECTED、CANCELLED、CHECK_OUT,因为只有占用的状态才参与冲突。

4.3 管理员端:审批、取消与练琴记录的接口实现

管理员端的核心操作是审批。审批时要防止“重复审批”覆盖状态,最稳的办法是条件更新,也就是在 SQL 里带上当前状态。代码示意:

public void approve(Long reservationId, Long operatorId, boolean pass) { // 超管权限校验省略 RoomReservation reservation = reservationMapper.selectById(reservationId); if (reservation == null || !"PENDING".equals(reservation.getStatus())) { throw new BizException("预约不存在或不在待审批状态"); } RoomReservation update = new RoomReservation(); update.setId(reservationId); update.setStatus(pass ? "APPROVED" : "REJECTED"); update.setAuditTime(LocalDateTime.now()); // 条件更新:只有当前状态仍是 PENDING 时才更新成功 LambdaUpdateWrapper<RoomReservation> wrapper = new LambdaUpdateWrapper<>(); wrapper.eq(RoomReservation::getId, reservationId) .eq(RoomReservation::getStatus, "PENDING"); reservationMapper.update(update, wrapper); }

这里先查一次再条件更新,是为了给出友好的业务提示。LambdaUpdateWrapper里带上status = 'PENDING',无论有多少个管理员同时操作,最终只有一条 SQL 能更新成功,事务提交后另一个请求更到的行数为 0,可以由此判断审批已失效。

琴房和公告的数据库增删改查就简单了,直接用 MyBatis Plus 提供的save、removeById、list方法即可。这些基础接口不需要手写 SQL,但要注意给管理员接口加一个角色校验,不能在 controller 里直接暴露deleteRoom给小程序的普通用户。

5. 微信小程序前端:页面实现与 5 个联调排查点

小程序端负责把后端能力变成可点击的界面,最常被折腾的是三个问题:自定义导航栏高度、时间段选择、登录态传递。下面逐个讲,最后给一份联调排查清单。

5.1 顶部导航栏高度适配与页面布局

默认导航栏在 Android 和 iPhone 上高度不同,如果直接用固定像素,总会出现标题偏上或按钮被顶出屏幕的情况。标准做法是用微信提供的菜单按钮位置反推导航栏高度。

function getNavBarInfo() { const system = wx.getSystemInfoSync(); const menu = wx.getMenuButtonBoundingClientRect(); const statusBarHeight = system.statusBarHeight || 20; const navBarHeight = (menu.top - statusBarHeight) * 2 + menu.height; return { statusBarHeight, navBarHeight }; }

说明:wx.getMenuButtonBoundingClientRect()返回胶囊按钮的位置和尺寸,menu.top - statusBarHeight得到胶囊距离状态栏底部的间距,这个间距在胶囊上方和下方几乎相等,所以乘以 2 再加上胶囊自身高度,就是整个自定义导航栏的高度。拿到这两个值后,在页面上把固定占位 view 的高度设为statusBarHeight + navBarHeight,内容区就不会被刘海屏遮住。这个方法在每次启动时调用一次就够了,不需要实时监听。

5.2 预约页面日期选择、时段计算与提交

预约页面通常是一个琴房详情页,用户点“预约”后弹出日期和时段选择。日期用picker mode="date",时段用两个picker mode="selector",分别选开始和结束。时段数组可以按 30 分钟粒度生成:

function generateSlots(startHour, endHour) { const slots = []; for (let h = startHour; h < endHour; h++) { const begin = h.toString().padStart(2, '0') + ':00'; const mid = h.toString().padStart(2, '0') + ':30'; slots.push(begin, mid); } slots.push(endHour.toString().padStart(2, '0') + ':00'); return slots; }

提交时把日期和两个时间拼成字符串传给后端:

wx.request({ url: baseUrl + '/api/reservations', method: 'POST', data: { roomId: this.data.roomId, reserveDate: this.data.date, // "2025-05-20" startTime: this.data.slots[this.data.startIndex], endTime: this.data.slots[this.data.endIndex] }, header: { 'Authorization': 'Bearer ' + wx.getStorageSync('token') }, success: (res) => { if (res.data.code === 200) { wx.showToast({ title: '预约成功' }); } else { wx.showModal({ content: res.data.message, showCancel: false }); } } });

这里的逻辑说明:时间全部用字符串传输,避免Date对象序列化带来时区偏移;res.data.message直接展示后端给的业务提示,比如“该时段已被预约”,前端不需要自己猜原因。后端如果返回 401,前端要跳回登录页重新wx.login。

5.3 登录获取手机号与联调合规注意

很多毕设要“获取微信手机号”,但现在这个能力有严格限制:小程序主体必须是企业或个体户,并且要完成微信认证,个人主体无法调用getPhoneNumber换取手机号。如果你的账号是个人开发者,最稳妥的做法是不要硬依赖手机号,改用wx.login静默登录,只绑定openid,用户表里的phone字段留空或让用户手动填。

部分源码包会写一个按钮模拟手机号登录流程,点击后触发wx.login拿code给后端,后端用code换openid并自动注册。这里要提醒一句,不要在前端写死一个“手机号”参数传给后端,那样既不合规,答辩也容易被老师一眼看穿。

<button open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber">手机号快捷登录</button>

如果确实已经具备企业认证主体,小程序端回调里拿到的是加密动态令牌e.detail.code,后端要拿这个code调微信接口换取真实手机号。但这里还需要同时配合wx.login的code,所以你会看到前端代码里经常同时出现两个code。毕业设计建议优先用静默登录方案,把精力放到预约流程上,不要在这里耗太久。

5.4 5 个联调排查点(现象 → 原因 → 解决)

联调阶段的问题往往比写代码时更折磨人,下面这几个是我见过的高频坑,也是最容易让演示视频翻车的点。

  1. 真机上所有请求都失败 现象:开发者工具里接口正常,一到真机预览,列表加载不出来。 原因:小程序要求后端域名必须是 HTTPS 且已在公众平台配置合法域名,没配置时真机会直接拦截请求。 解决:开发期在开发者工具“详情-本地设置”里勾选“不校验合法域名…”,线上则把后端放到带 HTTPS 证书的服务器上,并到公众平台添加 request 合法域名。

  2. 请求头里没有 token,接口全部 401 现象:用户明明已经登录,下一页面的接口还是报未登录。 原因:登录时把 token 存到了 storage,但每个wx.request没有统一带上。 解决:封装一个request函数,在发起请求前统一执行header.Authorization = 'Bearer ' + wx.getStorageSync('token')。

  3. 时间变成 “2025-05-20T14:00:00” 现象:预约记录里的开始时间显示成 ISO 格式,小程序端直接展示很丑。 原因:Jackson 默认把LocalDateTime序列化成 ISO 格式。 解决:在application.yml里配置spring.jackson.date-format=yyyy-MM-dd HH:mm:ss,或给字段加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")。

  4. 异步更新状态后页面不刷新 现象:用户提交预约成功后,返回列表页,数据还是旧的。 原因:前端没有在wx.request成功后刷新列表,或者说用wx.navigateBack返回时上一页不会自动触发 onLoad。 解决:在预约提交成功后调用getCurrentPages()拿到上一页实例,手动执行其onShow里的刷新方法。

  5. 预约总提示冲突,但明明该时段没人 现象:选了一个看起来完全没人的时段,提交后被后端拒绝。 原因:前后端对“某天某时”的理解不一致,比如后端用的是服务端时区,前端传来的日期经过解析后差了一天。 解决:统一使用YYYY-MM-DD字符串和HH:mm字符串,后端在比较前先LocalTime.parse清理掉秒数;同时给数据库连接参数serverTimezone=Asia/Shanghai。

这 5 条如果能提前避开,联调阶段会顺畅很多。前端页面的其他细节,比如空状态、加载动画、按钮防重复点击,都是加分项,但优先级低于这 5 条。

6. 打包、部署与答辩演示:怎么让毕设经得起追问

后端打包很简单,在项目根目录执行mvn clean package -DskipTests,生成的target目录下有一个 jar 包。本地跑起来用java -jar qinfang.jar,确认 8080 端口被监听后,小程序端把接口地址改成局域网 IP,真机预览时手机和电脑连同一个 Wi-Fi 就能联调。如果要部署到云服务器,可以用宝塔面板挂 jar,也可以写一个极简 Dockerfile,但答辩演示时本地运行反而更省事,不用怕网络波动。

小程序端上传体验版也很直接:在微信开发者工具里点“上传”,填版本号和备注,然后在公众平台的小程序版本管理里把版本设为体验版,生成二维码就能手机扫码。演示视频建议按这个流程录:学生进入小程序首页查看公告 → 选择琴房和时段发起预约 → 切换管理员账号审批通过 → 回到学生端看到状态变成已通过 → 到店点击签到 → 练琴结束后点击结束。整个过程控制在三分钟内,老师能一眼看到系统完整性。

答辩时老师大概率会问两个问题:一是“如何避免两个用户同时预约同一时段”,你可以直接回答用了事务 + 悲观锁,锁定的对象是琴房行,并补一句“如果未来要支撑高并发,可以升级为 Redis 分布式锁”;二是“状态机为什么这样设计”,你把PENDING → APPROVED → CHECK_IN → CHECK_OUT这条链路讲清楚,并说明每个状态对应的用户操作即可。

进阶方向也不难扩展:给预约表增加一个cancelled_reason字段,用于记录取消原因;用room_device表做设备报修;在管理后台用sys_notice发布公告,并在小程序首页轮播。这些改动不大,但能让系统看起来更完整。我自己做类似项目时,最后一次翻车就发生在并发预约上——学生端疯狂点击提交按钮,结果生成了两条预约记录,后来靠条件更新和状态校验兜住了。把这一步想清楚,你的琴房管理系统才能真的扛住答辩现场。希望帮到你。

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

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

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

立即咨询