ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

基于微信小程序的琴房预约管理系统:从数据库到冲突检测全解析

基于微信小程序的琴房预约管理系统:从数据库到冲突检测全解析 简介面向高校计算机相关专业毕业设计或课程设计场景这份微信小程序琴房管理系统项目包含完整源码、说明文档、LW及演示视频。系统按学生与管理员双角色设计学生端支持琴房查看、预约与公告浏览管理员端覆盖学生信息管理、琴房公告发布及登录注销等操作业务链路完整适合需要快速理解小程序全栈开发并产出毕设成果的读者。压缩包共1105个文件大小约25.4MB以vue、js、java、class等前后端源码文件为主辅以wxml/wxss小程序页面、json配置、sql数据库脚本、docx文档以及png/svg界面素材和mp4演示视频目录结构清晰、资源类型齐全。已有188人学习下载。包内还附带安装/运行/构建的bat脚本、关键文件备份与常见配置说明结合演示视频和说明文档可帮助读者梳理琴房预约的业务流程、接口设计及前后端交互细节便于二次开发与答辩准备。1. 琴房管理系统怎么做这个微信小程序项目到底解决什么问题琴房管理在很多学校还停留在「微信群接龙 管理员人工排表」的阶段学生接龙预约到了现场发现琴房被占用管理员每天花大量时间核对时间表、处理冲突。基于微信小程序的琴房管理系统把整套流程搬到线上——学生打开小程序查看琴房实时状态、按时间段预约管理员维护琴房、管理预约记录。这类项目常出现在毕业设计和课程设计中因为业务边界清晰又完整覆盖了小程序前端、服务端接口、数据库设计和定时任务几个模块。第一次做小程序全栈项目选它能把前后端基本功都练到评估它值不值得做核心就看一点预约冲突怎么用最少的代码挡住。本文把这条线从头到尾讲透。2. 需求拆解与数据库建模琴房预约的核心是时间冲突检测做这类系统别急着写代码先把业务流程和表结构想清楚。后台的编排决定了预约逻辑的复杂度一旦表结构定错后面改起来牵一发动全身。琴房管理系统翻来覆去就那么几个页面但真正考验设计能力的是预约这件事怎么用数据模型表达出来。2.1 三种角色与核心业务流程预约、取消、签到怎么串系统的角色其实就三类学生、管理员、定时任务。学生侧重在链路上走查看琴房列表 → 选择日期与时间段 → 提交预约 → 到琴房签到 → 完成使用。管理员侧重维护维护琴房信息、查看所有预约、异常情况下强制取消。定时任务做兜底把超时未签到的预约标记为爽约把过期预约归档。业务规则上我一般会按这几条定同一琴房同一日期同一时间段只能存在一条有效预约。预约开始时间必须早于结束时间且落在琴房营业时间内。只能预约未来 7 天内的时段不能预约过去的日期。取消预约必须在开始时间之前完成。预约开始后迟到 15 分钟未签到视为爽约。这些规则里最容易出错的是第 1 条——时间冲突。判断两个时间段是否重叠很多人第一反应是「新预约的开始时间是否落在已有预约的区间里」这个直觉是错的。正确的判定公式是只要新时段的开始早于已有时段的结束且新时段的结束晚于已有时段的开始就必然重叠。这个公式贯穿整篇文章后面的 SQL 和代码都在围着它转。2.2 用三张表把业务落地user、room、booking 的字段设计与索引核心就三张表字段不需要多够用就行。下面是推荐的表结构。-- 用户表 CREATE TABLE sys_user ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, openid VARCHAR(64) NOT NULL COMMENT 微信用户唯一标识, nickname VARCHAR(50) DEFAULT COMMENT 昵称, student_no VARCHAR(20) DEFAULT COMMENT 学号管理员可录入, role TINYINT NOT NULL DEFAULT 0 COMMENT 0-学生 1-管理员, status TINYINT NOT NULL DEFAULT 1 COMMENT 0-禁用 1-正常, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 琴房表 CREATE TABLE piano_room ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, room_name VARCHAR(50) NOT NULL COMMENT 房间名如 A101, location VARCHAR(100) DEFAULT COMMENT 位置描述, piano_type VARCHAR(30) DEFAULT COMMENT 钢琴类型, is_open TINYINT NOT NULL DEFAULT 1 COMMENT 0-关闭 1-开放, open_time TIME DEFAULT 08:00:00 COMMENT 营业开始, close_time TIME DEFAULT 22:00:00 COMMENT 营业结束, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT琴房表; -- 预约表 CREATE TABLE booking ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, room_id INT UNSIGNED NOT NULL COMMENT 琴房 id, user_id INT UNSIGNED NOT NULL COMMENT 用户 id, book_date DATE NOT NULL COMMENT 预约日期, start_time TIME NOT NULL COMMENT 开始时间, end_time TIME NOT NULL COMMENT 结束时间, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-已预约 1-已完成 2-已取消 3-爽约, remark VARCHAR(200) DEFAULT COMMENT 备注, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_room_time (room_id, book_date, start_time, end_time), KEY idx_user_status (user_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约表;几个设计要点说明。user 表的 openid 必须加唯一索引。微信小程序登录拿到的是 openid它是用户在当前小程序下的唯一身份标识每次登录都用它做插入或更新重复插入会直接报错也能防止同一个微信号注册出多个账号。booking 表的 idx_room_time 联合索引是冲突查询的关键。冲突检测的 SQL 会按 room_id 加 book_date 过滤再用 start_time 和 end_time 做区间比较这个索引能让查询走 range scan 而不是全表扫描。数据量几千条时差别不明显但到上万条后有没有这个索引就是毫秒级和秒级的差别。status 字段用 TINYINT 数字而不是字符串。毕业设计里很多人习惯存 booked、cancelled 这种字符串看着直观但索引效率和代码可读性都不如数字枚举。前后端约定好 0/1/2/3 的含义后端定义常量类统一使用比到处写魔法数强得多。2.3 接口清单与鉴权方式小程序端和服务端怎么分工接口按资源拆RESTful 风格就够不需要引入复杂框架。核心接口如下。接口方法核心入参说明/api/auth/loginPOSTcode用 wx.login 的 code 换登录态 token/api/room/listGET无琴房列表与开放状态/api/room/detailGETroomId琴房详情含营业时间与可预约时段/api/booking/createPOSTroomId、bookDate、startTime、endTime创建预约/api/booking/mineGET无当前用户的预约记录/api/booking/cancelPOSTbookingId取消预约/api/booking/checkinPOSTbookingId到琴房签到/api/admin/room/savePOST琴房信息管理员维护琴房鉴权有两种常见做法。轻量一点的是后端生成随机 token 存到 Redis设过期时间小程序每次请求把 token 放在 Authorization 头里重一点的是直接上 JWT登录时签发过期时间由服务端控制。对这个项目来说Redis token 方案更直观也方便管理员端做强制下线我一般会用这种。后端用拦截器统一校验白名单放行登录接口其余接口都要求携带有效 token管理员接口再额外查一次 role 字段。3. 小程序端实现登录态、琴房列表和预约提交的关键代码小程序端的核心页面就三个琴房列表页、预约页、我的预约页。这一章把代码拆开讲从登录态到预约提交每一步给出能直接用的片段。所有请求都走 wx.request业务层把 url 和 data 组织好就行。3.1 微信登录与 Token 会话管理code 换 token 的完整链路微信小程序的登录链路是小程序调用 wx.login 拿到一次性 code后端拿 code 去微信接口换 openid 和 session_key再生成业务 token 返回给小程序。code 只能用一次且有效期很短所以小程序端不能缓存 code每次启动都要重新调用 wx.login。// miniprogram/utils/auth.js const TOKEN_KEY token; function login() { return new Promise((resolve, reject) { wx.login({ success: (res) { if (!res.code) { reject(new Error(wx.login 未返回 code)); return; } wx.request({ url: getApp().globalData.apiBase /api/auth/login, method: POST, data: { code: res.code }, success: (resp) { if (resp.data.code 0) { const token resp.data.data.token; wx.setStorageSync(TOKEN_KEY, token); resolve(resp.data.data); } else { reject(new Error(resp.data.msg || 登录失败)); } }, fail: (err) reject(err) }); }, fail: (err) reject(err) }); }); } function getToken() { return wx.getStorageSync(TOKEN_KEY); } module.exports { login, getToken, TOKEN_KEY };逻辑说明wx.login 成功后拿到 code把它 POST 给后端后端用 code 换 openid 后生成 token 返回。前端把 token 放进本地缓存后续所有请求从缓存取。参数上注意 apiBase 是后端服务地址开发阶段指向局域网 IP 加端口正式上线必须换成已配置 HTTPS 证书的域名。提示token 失效时后端通常会返回 401小程序端应该封装一个请求函数统一拦截 401 后自动重新登录并重放原请求。这个习惯能帮你省掉大量「登录态过期」导致的现场翻车。关于 session_key它用于解密微信加密数据比如手机号。琴房管理系统如果只是用 openid 做身份识别session_key 在后端换完就可以丢弃不用存。见过不少同学把 session_key 也写进 user 表没有必要除非你还要做微信手机号授权。3.2 琴房列表与状态展示拉取数据、渲染与下拉刷新琴房列表页的要点是状态展示。后端返回的 room 里带 status 字段前端要把它翻译成用户能看懂的文字和颜色而不是直接把数字渲染到页面上。// pages/room/room.js const { getToken } require(../utils/auth.js); Page({ data: { rooms: [], loading: false }, onShow() { this.fetchRooms(); }, onPullDownRefresh() { this.fetchRooms(true); }, fetchRooms(isRefresh false) { if (this.data.loading) return; this.setData({ loading: true }); wx.request({ url: getApp().globalData.apiBase /api/room/list, header: { Authorization: Bearer getToken() }, success: (res) { if (res.data.code 0) { const rooms res.data.data.map((room) ({ ...room, statusText: this.statusText(room.status), statusClass: room-status- room.status })); this.setData({ rooms }); } else { wx.showToast({ title: res.data.msg || 加载失败, icon: none }); } }, complete: () { this.setData({ loading: false }); if (isRefresh) wx.stopPullDownRefresh(); } }); }, statusText(status) { const map { 0: 空闲, 1: 使用中, 2: 维护中 }; return map[status] || 未知; }, goBook(e) { const roomId e.currentTarget.dataset.id; wx.navigateTo({ url: /pages/book/book?roomId roomId }); } });对应的 WXML 片段view classroom-card wx:for{{rooms}} wx:keyid view classroom-name{{item.roomName}}/view view classroom-location{{item.location}} · {{item.pianoType}}/view view classroom-status {{item.statusClass}}{{item.statusText}}/view button typeprimary sizemini bindtapgoBook>// pages/book/book.js const { getToken } require(../utils/auth.js); const SLOT_MINUTES 60; const MAX_ADVANCE_DAYS 7; Page({ data: { roomId: , room: null, dateList: [], dateIndex: 0, slotList: [], slotIndex: -1, submitting: false }, onLoad(options) { this.setData({ roomId: options.roomId }); this.buildDateList(); this.loadRoomDetail(); }, buildDateList() { const list []; const week [日, 一, 二, 三, 四, 五, 六]; for (let i 0; i MAX_ADVANCE_DAYS; i) { const d new Date(); d.setDate(d.getDate() i); const dateStr ${d.getFullYear()}-${String(d.getMonth() 1).padStart(2, 0)}-${String(d.getDate()).padStart(2, 0)}; const label i 0 ? 今天 : i 1 ? 明天 : ${d.getMonth() 1}月${d.getDate()}日 周${week[d.getDay()]}; list.push({ dateStr, label }); } this.setData({ dateList: list }); }, loadRoomDetail() { wx.request({ url: getApp().globalData.apiBase /api/room/detail, data: { roomId: this.data.roomId }, header: { Authorization: Bearer getToken() }, success: (res) { if (res.data.code 0) { this.setData({ room: res.data.data }); this.buildSlotList(); } } }); }, buildSlotList() { const room this.data.room; if (!room) return; const slots []; const openHour parseInt(room.openTime.slice(0, 2), 10); const closeHour parseInt(room.closeTime.slice(0, 2), 10); for (let h openHour; h SLOT_MINUTES / 60 closeHour; h) { const start ${String(h).padStart(2, 0)}:00:00; const end ${String(h SLOT_MINUTES / 60).padStart(2, 0)}:00:00; slots.push({ start, end, label: ${start.slice(0, 5)} - ${end.slice(0, 5)} }); } this.setData({ slotList: slots }); }, onDateChange(e) { this.setData({ dateIndex: Number(e.detail.value), slotIndex: -1 }); }, onSlotSelect(e) { this.setData({ slotIndex: Number(e.currentTarget.dataset.index) }); }, submit() { const { roomId, dateList, dateIndex, slotList, slotIndex, submitting } this.data; if (slotIndex 0) { wx.showToast({ title: 请选择时间段, icon: none }); return; } if (submitting) return; this.setData({ submitting: true }); const date dateList[dateIndex].dateStr; const slot slotList[slotIndex]; wx.request({ url: getApp().globalData.apiBase /api/booking/create, method: POST, header: { Authorization: Bearer getToken() }, data: { roomId: roomId, bookDate: date, startTime: slot.start, endTime: slot.end }, success: (res) { if (res.data.code 0) { wx.showToast({ title: 预约成功, icon: success }); setTimeout(() wx.navigateBack(), 1500); } else { wx.showToast({ title: res.data.msg || 预约失败, icon: none }); } }, complete: () { this.setData({ submitting: false }); } }); } });逻辑说明日期生成用循环加 7 天第一二天显示「今天」「明天」后面的显示具体日期和星期这个细节在演示时很加分。时间片按 room.openTime 和 closeTime 解析出小时数循环生成整点时段。submitting 标志防止用户重复点击提交这个在预约场景里是必须的不然一次点击发出两个请求就会出现「我只点了一次怎么订了两条」。注意一个边界时间片生成只考虑了整点如果琴房营业时间是 08:30 到 21:30这种写法生成的时段会不准。稳妥的做法是后端在 room/detail 接口里直接返回可预约时间片数组前端只负责渲染。这样营业时间、粒度、特殊日期的规则全部收敛在后端一处前端永远不用自己去推。4. 后端服务与冲突控制把预约规则写成可执行的代码后端是这类系统的重头戏。预约创建、冲突检测、超时释放这三块写好了项目核心逻辑就立住了。这一章以 Java Spring Boot 的写法为例其他语言思路一致SQL 部分可以直接搬过去。4.1 预约创建的完整流程参数校验、冲突检测与事务预约创建的接口不能只是简单 insert它要按顺序做五件事参数校验、琴房校验、营业时间校验、冲突检测、插入。任何一步失败都返回业务错误。Transactional(rollbackFor Exception.class) public Booking createBooking(BookingDTO dto, Long userId) { // 1. 基本参数校验 if (dto.getStartTime().compareTo(dto.getEndTime()) 0) { throw new BusinessException(开始时间必须早于结束时间); } if (dto.getBookDate().isBefore(LocalDate.now())) { throw new BusinessException(不能预约过去的日期); } // 2. 琴房存在且开放 PianoRoom room roomMapper.selectById(dto.getRoomId()); if (room null || room.getIsOpen() ! 1) { throw new BusinessException(琴房不存在或未开放); } // 3. 时间必须落在琴房营业时间范围内 if (dto.getStartTime().isBefore(room.getOpenTime()) || dto.getEndTime().isAfter(room.getCloseTime())) { throw new BusinessException(预约时间超出琴房营业时间); } // 4. 行锁 冲突检测 int conflict bookingMapper.countConflictForUpdate( dto.getRoomId(), dto.getBookDate(), dto.getStartTime(), dto.getEndTime()); if (conflict 0) { throw new BusinessException(该时间段已被预约请选择其他时间); } // 5. 插入预约记录 Booking booking new Booking(); booking.setRoomId(dto.getRoomId()); booking.setUserId(userId); booking.setBookDate(dto.getBookDate()); booking.setStartTime(dto.getStartTime()); booking.setEndTime(dto.getEndTime()); booking.setStatus(BookingStatus.BOOKED); bookingMapper.insert(booking); return booking; }逻辑说明第 1 步挡住「开始晚于结束」和「预约过去日期」两种低级错误这类请求根本不该进到数据库。第 3 步校验营业时间前端选了 21:30 但琴房 21:00 关门这里直接拒绝。第 4 步是核心countConflictForUpdate 会先锁住相关记录再统计避免两个并发请求同时通过检查。第 5 步插入后事务提交整个过程要么全部成功要么全部回滚不会出现「冲突检测通过了但插入失败」的脏状态。注意Transactional 一定要加 rollbackFor Exception.class。Spring 默认只在 RuntimeException 时回滚抛自定义 checked 异常时事务不回滚数据就写进去了这个细节坑过很多人。还有一个容易踩的坑有的同学把冲突检测拆成独立 service 方法然后在 createBooking 里调用。如果这个方法没有事务传播检查完再插入的间隙另一个请求可能已经插入了同一时段。所以检测和插入必须在同一个事务方法里这是硬要求。4.2 冲突检测 SQL 的正确写法区间重叠判定与索引利用冲突检测的 SQL 是全文最重要的几行代码。select idcountConflictForUpdate resultTypeint SELECT COUNT(*) FROM booking WHERE room_id #{roomId} AND book_date #{bookDate} AND status 0 AND start_time lt; #{endTime} AND end_time gt; #{startTime} FOR UPDATE /select逻辑说明新预约时段是 [startTime, endTime]已有预约时段是 [start_time, end_time]。两者重叠的充要条件是新开始时间早于已有结束时间且新结束时间晚于已有开始时间。写成 SQL 就是 start_time #{endTime} AND end_time #{startTime}。再加 book_date 限定同一天status 0 只统计有效预约排除已取消和已归档的记录。反例值得单独说。有同学会写成下面这两种-- 反例一只覆盖「完全包含」的情况 WHERE start_time #{startTime} AND end_time #{endTime} -- 反例二只看已有预约的开始时间是否落在新时段内 WHERE start_time BETWEEN #{startTime} AND #{endTime}反例一漏掉了「新时段横跨已有时段」的部分重叠反例二漏掉了「已有时段横跨新时段」的场景。实测中这两种写法会各放走一批冲突预约等管理员在后台看到记录重了再去手工取消就很被动。FOR UPDATE 是 MySQL InnoDB 的行锁语法配合 idx_room_time 索引它会锁住命中的索引区间。并发场景下两个请求同时查同一琴房同一天的数据后到的会等先到事务提交后才继续这样冲突检测就变成串行执行不会出现两人同时订走同一时段的情况。这个锁粒度对这个项目足够没必要上 Redis 分布式锁。idx_room_time 联合索引room_id, book_date, start_time, end_time覆盖了查询的过滤条件MySQL 会走索引 range scan数据量到十万级也不会慢。如果这个项目用于期末设计这属于「可展示的性能设计点」答辩时值得拿出来讲。4.3 超时释放与签到定时任务让预约状态自动流转预约状态不能永远停在「已预约」。到了时间没来系统要自动释放琴房并标记爽约当天用完了要标记完成。这些靠定时任务和签到接口配合完成。Component public class BookingScheduler { Autowired private BookingMapper bookingMapper; // 每分钟跑一次已预约但开始时间已过 15 分钟的标记为爽约 Scheduled(cron 0 * * * * ?) public void markNoShow() { ListBooking list bookingMapper.findExpiredNoShows(); for (Booking b : list) { b.setStatus(BookingStatus.NO_SHOW); bookingMapper.updateById(b); } } // 每天凌晨 2 点把前一天的记录批量归档为已完成 Scheduled(cron 0 0 2 * * ?) public void archiveYesterday() { bookingMapper.archiveByDate(LocalDate.now().minusDays(1)); } }对应的 SQL-- 找出已超时未签到的预约 SELECT * FROM booking WHERE status 0 AND (book_date CURDATE() OR (book_date CURDATE() AND start_time DATE_SUB(CURTIME(), INTERVAL 15 MINUTE)))逻辑说明markNoShow 的判定条件是「开始时间已过去 15 分钟且状态仍是已预约」。这里故意留 15 分钟缓冲学生可能因为路上耽搁晚到几分钟直接到点就释放很容易引发投诉。archiveYesterday 是每天凌晨的兜底任务把前一天未完成的记录批量归档。cron 表达式里0 * * * * ?表示每分钟第 0 秒执行0 0 2 * * ?表示每天凌晨 2 点整执行。签到接口单独说一下。学生到琴房后点「签到」后端要校验该预约属于当前用户、状态是已预约、当前时间不早于预约开始时间然后状态改成已完成。有个细节容易漏预约明天 10:00现在签到必须拒绝预约今天 10:00 而现在是 10:05允许签到。只判断「日期相等」而忽略当前时刻会导致预约还没开始就能提前签到的笑话逻辑。到这里系统核心链路已经通了前端选时间、后端挡住冲突、定时任务兜底释放。第 5 章把开发中最高发的几个问题集中排一遍雷。5. 琴房管理系统避坑指南5 个高发问题与排查方法这一章按「现象 → 原因 → 解决」的格式写都是这个项目里反复出现的问题每一类我都见过不止一次。你如果正在调试直接对照着查比从头翻日志快得多。5.1 冲突检测漏掉「部分重叠」导致重复预约现象A 同学约了 10:00-11:00B 同学竟然还能约 10:30-11:30后台出现两条重叠预约。原因冲突检测 SQL 写成了 start_time #{startTime} AND end_time #{endTime}只判断新时段是否完整包含在已有时段内。B 的 10:30-11:30 和 A 的 10:00-11:00 是交叉重叠新开始 10:30 落在 A 的时段内但新结束 11:30 超出了 A 的结束时间条件不成立于是被放行。解决改用区间重叠判定 start_time #{endTime} AND end_time #{startTime}。改完用三种用例验证新时段在已有时段内部、新时段横跨已有时段、新时段与已有时段首尾相接前两个必须返回冲突第三个在边界恰好相切时允许通过。我一般会把这几个用例写进单元测试防止后续改动时悄悄回归。5.2 并发预约「超卖」同一时段被两人同时订走现象两个学生同时间隔几百毫秒提交同一琴房同一时段的预约两个接口都返回成功数据库里两条记录。原因没有加锁的「先查询后插入」存在竞态窗口。请求 A 查到冲突数为 0还没插入请求 B 也查到冲突数为 0接着两个请求都执行插入冲突检测形同虚设。解决在冲突检测 SQL 末尾加 FOR UPDATE并让检测与插入处于同一事务中。后到的请求会等待先到事务提交后再执行查询此时冲突数变成 1预约被拒绝。如果项目用的是 PostgreSQL对应语法是 FOR KEY SHARE如果不想依赖数据库锁可以在 booking 表加唯一索引room_id, book_date, start_time做兜底插入冲突时直接报错再用异常重试逻辑处理。5.3 预约时间凭空差 8 小时数据库时区问题现象小程序端选了 10:00 提交数据库里存的是 02:00管理后台看到的预约时间全部不对。原因Java 的 LocalDateTime 和 MySQL 的 DATETIME 在时区解析上出现偏差。常见的是 JDBC 连接串里没有指定 serverTimezone驱动用了默认时区跟服务器系统时区不一致导致时间偏移。解决在 JDBC 连接串上显式指定时区写成 jdbc:mysql://localhost:3306/piano?serverTimezoneAsia/Shanghai。同时约定前后端传输时间一律用字符串格式如 10:00:00不传时间戳。TIME 类型本身不涉及时区换算用字符串传递能避开大部分时区坑。这属于典型的「开发环境不出错、一换机器就翻车」的问题答辩前一定要换台机器部署验证一遍。5.4 开发者工具正常、真机请求失败域名与 HTTPS现象微信开发者工具里所有接口都通扫码到真机上一片空白控制台报 request:fail url not in domain list。原因小程序真机环境强制要求后台接口走 HTTPS且域名必须在小程序后台配置为 request 合法域名。开发工具默认不校验合法域名本地 http 接口能通但真机不认。解决开发阶段在开发者工具的详情设置里勾选「不校验合法域名、TLS 版本以及 HTTPS 证书」这只解决本机调试。要真机完整跑通需要准备一个已配置 HTTPS 证书的域名在小程序后台配好 request 合法域名再把后端接口地址从局域网 IP 换成线上域名。证书和域名校验是硬条件没有捷径。演示视频建议在真机网络通畅时录制别在开发工具里录两者观感差很多。5.5 前端选了合法时间、后端却报「超出营业时间」现象前端时间选择器最多到 21:00但用户改一下请求参数就能提交 22:00 的预约或者前端按 60 分钟生成时间片后端允许提交 30 分钟的时段预约粒度混乱。原因前端的时间组装逻辑和后端校验规则是两套代码没有统一。用户只要绕过前端界面直接调接口就能提交任意时间。解决把「可预约时段」的完整定义收敛到后端。常见做法是后端 room/detail 接口直接返回该琴房可预约的时间片数组前端渲染时直接用不做二次计算后端 create 接口再做兜底校验即使前端被改非法时间也会被拒绝。一句话前端做体验后端做规则互不越界。6. 让项目在答辩现场不翻车预置数据与演示路径项目能跑和能演示是两回事。很多同学代码写完了上台演示时现场预约、现场等数据结果网络慢、时间对不上、页面转圈。提前预置数据是最有效的规避手段。预置数据的核心原则让演示路径上的每一步都有「看起来真实」的数据支撑。下面这组 SQL 是三张表的最小演示数据集。-- 琴房 INSERT INTO piano_room (room_name, location, piano_type, is_open, open_time, close_time) VALUES (A101, 1号楼 1层, 立式钢琴, 1, 08:00:00, 22:00:00), (A102, 1号楼 1层, 三角钢琴, 1, 08:00:00, 22:00:00), (B201, 2号楼 2层, 电子钢琴, 0, 09:00:00, 21:00:00); -- 用户 INSERT INTO sys_user (openid, nickname, role, status) VALUES (demo_admin_openid, 管理员, 1, 1), (demo_student_openid, 演示学生, 0, 1); -- 预约一条待使用、一条已取消、一条已完成 INSERT INTO booking (room_id, user_id, book_date, start_time, end_time, status) VALUES (1, 2, DATE_ADD(CURDATE(), INTERVAL 1 DAY), 10:00:00, 11:00:00, 0), (1, 2, DATE_ADD(CURDATE(), INTERVAL 1 DAY), 14:00:00, 15:00:00, 2), (2, 2, DATE_ADD(CURDATE(), INTERVAL -1 DAY), 08:00:00, 09:00:00, 1);预约数据里用 DATE_ADD(CURDATE(), INTERVAL 1 DAY) 替代写死的日期这个细节很关键它保证任何一天打开演示环境数据都是「明天有预约」不会因为时间推移全部过期。演示数据永远新鲜这是答辩演示最省心的一条技巧。演示路径我一般按这个顺序走登录进首页看到三间琴房状态各异 → 点开 A101 看到明天的时段列表 → 选 10:00-11:00 提交预约成功 → 切到「我的预约」看到刚刚的记录 → 取消再重新预约 → 打开管理员视角看全部预约记录。整条路径覆盖登录、查询、创建、取消、管理五个核心功能五分钟讲完节奏刚好。另一个容易忽略的点提前把演示用的账号登录态保持住别在现场等 wx.login 转圈。正式演示前五分钟在演示手机上完整走一遍登录流程确认 token 有效再把小程序切到后台待命。现场只做操作不做登录。做这个项目时我最大的教训是把「代码写完」当成「项目做完」结果第一次联调时被冲突检测的边界条件打脸。如果你正在做这个方向我建议把第 4 章的三种重叠用例写进测试跑通了再谈演示。开发里多留一分敬畏答辩时就少一分慌张。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表