
很多做计算机毕设的同学看到“共享茶室预约”这类题目第一反应是“不就是个增删改查吗”。真动手之后才发现微信小程序端、Java后端、管理后台三块堆在一起光是把预约的时间冲突逻辑理清楚就够喝一壶。这篇文章我就以过来人的身份把整个项目从选型、建模、编码到排错的全过程拆开讲包括为什么用Spring Boot MyBatis Plus组合、预约时段到底按半小时还是按小时切、并发下单怎么防超卖、微信登录的code2Session踩坑点在哪里。无论你是想直接参考这套设计完成毕设还是打算在此基础上加功能比如拼团、会员卡、积分这篇都能给你一条能落地的路线。1. 项目定位与核心需求拆解毕设题目的真实考察点先说句掏心窝的话毕设题目的名字越长往往意味着评委想看到的“工程量”越完整。“基于Java的共享茶室在线预约微信小程序设计与实现”这个题目拆开看其实是三个必须同时交付的部分——小程序端用户怎么约、Java服务端约了之后怎么处理、管理端茶室老板怎么管。只做一个端答辩的时候基本站不住脚。1.1 从题目关键词反推技术栈要求题目里“Java”和“微信小程序”是硬性限定这决定了你没法用PHP或者Node.js糊弄后端。我当时的选型是后端框架Spring Boot 2.7.x。理由很实在Spring Boot的自动配置能让项目在半小时内跑起来内嵌Tomcat省去部署麻烦而且网上资料最多遇到问题搜一下就有解。Spring MVC那套注解RestController、RequestMapping对于刚接触Java Web的同学来说学习曲线比SSHStrutsSpringHibernate时代平滑太多。持久层MyBatis Plus。我选它不是因为MyBatis不好而是因为单表CRUD场景下MyBatis Plus的BaseMapper直接帮你把insert、update、selectById这些都写好了你能把精力省下来放在预约冲突、订单状态流转这些真正有含金量的逻辑上。数据量就几千条不存在性能瓶颈这点可以放心用。数据库MySQL 8.0Navicat做可视化操作。为什么不用Oracle毕设场景杀鸡不用牛刀MySQL完全够用而且云数据库的学生优惠价格很低。小程序端原生微信开发者工具开发不推荐用uni-app。原因后面专门讲这里先记住结论毕设答辩时老师大概率会问“你熟悉小程序生命周期吗”原生开发答起来才有底气。如果学校要求必须有“创新点”我建议在小程序端加一个基于噪声传感器或摄像头画面判断的“环境拥挤度展示”功能把物联网概念揉进去这属于硬件结合软件的方向答辩时很加分。1.2 用户角色和核心业务流程共享茶室和普通茶馆最大的区别在于“无人值守”——用户在小程序上完成预约、扫码开门、自助泡茶、按时付费整个过程不依赖前台人员。所以系统必须拆成三种角色用户C端微信授权登录、浏览茶室列表、查看空闲时段、提交预约、在线支付、查看预约记录、申请退款不退款。茶室管理员B端管理茶室信息名称、位置、茶桌数、环境照片、设置营业时段、修改预约价格、查看每日订单流水。系统管理员审核茶室入驻、管理所有用户、处理异常订单。这块很多同学会漏掉但评委很看重“平台级”思维。核心流程一句话概括就是用户选茶室 → 选日期和时段 → 提交订单 → 支付 → 到店扫码或输入密码开门 → 使用结束后系统自动扣费超时部分另算。这里有个关键设计预约后是“锁定时段”而不是“进入即计时”。茶室按小时段出租用户预约的是某个时间段的使用权。所以订单表里必须有startTime和endTime两个字段后端在下单时就要判断这两个时间点之间是否已被其他订单占用。2. 数据库设计与核心表结构需求分析能力的直接体现很多同学一上来就建表结果做完发现缺这缺那。正确做法是先画ER图实体关系图理清实体之间的关联再动手建库。我当时整理出来的核心表有8张这里挑4张最关键的详细说。2.1 用户表user与微信登录的字段设计CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, openid varchar(64) NOT NULL COMMENT 微信openid唯一标识, nickname varchar(64) DEFAULT NULL COMMENT 微信昵称, avatar_url varchar(255) DEFAULT NULL COMMENT 头像地址, phone varchar(20) DEFAULT NULL COMMENT 手机号, balance decimal(10,2) DEFAULT 0.00 COMMENT 账户余额单位元, status tinyint(4) DEFAULT 1 COMMENT 状态1正常0禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;表结构本身中规中矩但有两个细节我得提醒你第一openid必须加唯一索引。微信小程序每个用户对应唯一的openid这是用户身份识别的凭证。很多同学用自增id关联业务表其实更稳妥的做法是把openid作为逻辑外键。但考虑到查询效率openid长度64字符比bigint慢一些我最后采用的方式是业务表统一存user_id通过user表反查openid。第二balance字段是留着做“余额支付”扩展用的。就算你第一期不做钱包功能也建议提前把这个字段加上否则后期加需求得改表结构麻烦得很。2.2 茶室表tea_room与时段配置的联动设计CREATE TABLE tea_room ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 茶室名称, address varchar(255) DEFAULT NULL COMMENT 详细地址, latitude decimal(10,6) DEFAULT NULL COMMENT 纬度, longitude decimal(10,6) DEFAULT NULL COMMENT 经度, cover_url varchar(255) DEFAULT NULL COMMENT 封面图, description text COMMENT 茶室简介, price_per_hour decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 每小时价格元, open_time time NOT NULL DEFAULT 09:00:00 COMMENT 营业开始时间, close_time time NOT NULL DEFAULT 22:00:00 COMMENT 营业结束时间, status tinyint(4) DEFAULT 1 COMMENT 状态1营业中0休息, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这张表的关键是营业时段与预约时段的取舍。如果不限定startTime和endTime用户可以选择任意时间段这对系统复杂度是灾难性的。我见过一个同题目的同学前端时间选择器用的是自由时间范围后端做冲突检测时发现可预约状态是一个复杂的区间计算问题最后草草收场。我的做法是让茶室按小时段营业用户预约时必须选择某个小时段比如14:00-15:00一次可连续预约多个小时段。这样既符合共享茶室的实际消费习惯大多数茶客一坐就是一两个小时又让算法从“区间相交判断”简化为“逐小时槽位判断”逻辑清晰很多。具体实现小程序端生成当天可预约的小时列表10:00-11:00、11:00-12:00...用户点选想要的时段加入购物篮最后统一提交。2.3 预约订单表booking_order状态机设计是核心CREATE TABLE booking_order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单编号业务展示用, user_id bigint(20) NOT NULL COMMENT 下单用户ID, room_id bigint(20) NOT NULL COMMENT 茶室ID, booking_date date NOT NULL COMMENT 预约日期, start_time time NOT NULL COMMENT 开始时间, end_time time NOT NULL COMMENT 结束时间, total_amount decimal(10,2) NOT NULL COMMENT 订单总金额, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态0待支付1已支付2已完成3已取消4已退款, pay_time datetime DEFAULT NULL COMMENT 支付时间, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_room_date (room_id,booking_date), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;status字段是这张表的灵魂。我把订单状态定义为一个“有限状态机”每个状态都有明确的迁移条件待支付0用户提交预约订单但未付款。为了防止恶意占座待支付订单只保留15分钟超时自动取消并释放时段。已支付1支付成功后进入该状态茶室时段锁定不可再被预约。已完成2使用时间结束后系统自动或用户手动确认完成。这里我加了一个定时任务每5分钟扫描一次将end_time小于当前时间的“已支付”订单自动置为“已完成”防止用户忘记确认导致状态卡死。已取消3用户在预约开始前取消或超时未支付自动取消。已退款4取消后原路退回款项或者管理员后台手动退款。这里有一个实战经验状态迁移必须做幂等保护。比如你接了支付回调万一微信服务器因为网络原因多次回调你的接口代码里没做状态判断就会把订单从“已支付”改到“已完成”再改到“已退款”直接炸掉。所以每次更新状态前先查一次当前状态只有符合迁移条件的才允许更新用UPDATE语句的WHERE条件去控制比如UPDATE booking_order SET status1 WHERE id? AND status0。2.4 时段占用表room_slot防止超卖的关键CREATE TABLE room_slot ( id bigint(20) NOT NULL AUTO_INCREMENT, room_id bigint(20) NOT NULL COMMENT 茶室ID, slot_date date NOT NULL COMMENT 日期, slot_hour tinyint(4) NOT NULL COMMENT 小时如14表示14:00-15:00, order_id bigint(20) DEFAULT NULL COMMENT 占用的订单IDNULL表示空闲, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0空闲1占用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_room_date_hour (room_id,slot_date,slot_hour) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这张表是解决高并发下订单冲突问题的核心手段。共享茶室和电影院很像同一个房间的同一个小时段只能被一个订单占用。所以我在room_slot上建立了唯一索引uk_room_date_hour数据库层面强约束。用户提交订单时后端做三步操作遍历用户选择的所有小时段逐条插入room_slot设置了order_id为本次订单号如果任何一条插入失败违反唯一索引说明该时段已被别人抢走抛出异常回滚前面所有成功的插入全部插入成功后再生成订单。配合Spring的Transactional注解这三步在同一个事务里执行。因为唯一索引的存在即使两个用户同时点击“提交订单”数据库也只会允许其中一个插入成功另一个被拒绝。这就是我在评论区看到不少人问“如何防止同一时段被重复预约”的答案——不是靠Java代码里的synchronized而是靠数据库层面的唯一约束这是最简单也最可靠的做法。如果你后续想扩展成“预约时可选择不同茶桌”只需要把room_slot表加一个desk_id字段唯一索引相应改成(room_id, desk_id, slot_date, slot_hour)即可。3. 后端核心接口实现从微信登录到预约下单的完整链路后端项目我采用标准的Controller-Service-Mapper三层结构。Controller负责接收参数和返回结果Service承载业务逻辑Mapper和数据库打交道。下面按业务链路逐段说明。3.1 微信登录code2Session是唯一正确姿势小程序端调用wx.login()获取临时code把code发给后端后端拿着code去微信的接口换openid和session_key。很多同学第一次写的时候容易犯一个错误在小程序端直接调微信的code2Session接口。这绝对不行——因为session_key涉及用户身份不能暴露给前端而且小程序请求域名必须是HTTPS白名单内的微信接口域名不在白名单里会直接失败。正确的后端代码是RestController RequestMapping(/api/auth) public class AuthController { Autowired private AuthService authService; PostMapping(/login) public Result login(RequestBody LoginRequest request) { // request.getCode() 是小程序端wx.login()获取到的临时code return Result.success(authService.login(request.getCode())); } }Service里的核心逻辑public String login(String code) { // 1. 调用微信接口code2Session String url https://api.weixin.qq.com/sns/jscode2session?appid appId secret appSecret js_code code grant_typeauthorization_code; // 用RestTemplate发GET请求解析返回的json // 返回结果里有openid、session_key String openid parseOpenid(sendGetRequest(url)); // 2. 查数据库不存在则注册新用户 User user userMapper.selectByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); userMapper.insert(user); } // 3. 生成自定义登录态token后续请求带着这个token // 这里用JWT简单实现把userId塞进token设置7天过期 return JwtUtil.createToken(user.getId()); }这里要特别提醒不要用session_key来维持用户登录态。session_key有效期不固定且只在特定时候能用。我统一用JWTJSON Web Token做鉴权后端写一个拦截器每次请求从Header里取出token校验合法后把userId放入ThreadLocal供后续业务使用。JWT依赖引入dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency3.2 获取可预约时段动态计算而不是硬编码小程序端需要展示“今天能约哪些小时”这个接口看起来简单其实有讲究。你要根据当前时间动态过滤掉已过去的时段public ListHourSlot getAvailableSlots(Long roomId, String dateStr) { LocalDate date LocalDate.parse(dateStr); LocalTime now LocalTime.now(); // 获取茶室的营业时间比如09:00-22:00 TeaRoom room teaRoomMapper.selectById(roomId); LocalTime open room.getOpenTime().toLocalTime(); LocalTime close room.getCloseTime().toLocalTime(); ListHourSlot slots new ArrayList(); for (int hour open.getHour(); hour close.getHour(); hour) { HourSlot slot new HourSlot(); slot.setHour(hour); slot.setLabel(hour :00- (hour 1) :00); // 如果选的是今天已经过去的时段不可选 if (date.equals(LocalDate.now()) hour now.getHour()) { slot.setAvailable(false); } else { // 查room_slot表判断该时段是否被占 int count roomSlotMapper.countByRoomDateHour(roomId, date, hour); slot.setAvailable(count 0); } slots.add(slot); } return slots; }注意这里“hour now.getHour()”的判断条件。如果你营业时间是到22:00而现在是14:30那么14:00-15:00这个时段正在被使用中15:00-16:00及以后的时段才能约。也可以更保守一点把now.getMinute() 0的情况算作下一个小时也不可约比如14:30时把14、15两个时段都禁掉。这块由你来定毕竟茶室老板有打扫整理的时间。我最后选择的方案是当前小时不可约下个小时可约比较合理。3.3 下单接口事务、唯一索引、幂等三重保障这个接口是最容易写翻车的。我给出的写法如下Transactional(rollbackFor Exception.class) public BookingOrder createOrder(Long userId, Long roomId, String bookingDate, ListInteger hours) { // 1. 先查茶室确认存在且营业中 TeaRoom room teaRoomMapper.selectById(roomId); if (room null || room.getStatus() 0) { throw new BusinessException(茶室不存在或已打烊); } // 2. 生成订单号格式yyyyMMddHHmmss 随机4位 String orderNo generateOrderNo(); // 3. 计算总金额 BigDecimal totalAmount room.getPricePerHour() .multiply(BigDecimal.valueOf(hours.size())); // 4. 插入订单状态为待支付 BookingOrder order new BookingOrder(); order.setOrderNo(orderNo); order.setUserId(userId); order.setRoomId(roomId); order.setBookingDate(LocalDate.parse(bookingDate)); order.setStartTime(LocalTime.of(hours.get(0), 0)); order.setEndTime(LocalTime.of(hours.get(hours.size() - 1) 1, 0)); order.setTotalAmount(totalAmount); order.setStatus(0); bookingOrderMapper.insert(order); // 5. 插入时段占用记录这一步失败会触发回滚 for (Integer hour : hours) { RoomSlot slot new RoomSlot(); slot.setRoomId(roomId); slot.setSlotDate(LocalDate.parse(bookingDate)); slot.setSlotHour(hour); slot.setOrderId(order.getId()); slot.setStatus(1); roomSlotMapper.insert(slot); // 唯一索引冲突则抛异常 } return order; }这段代码有三个设计精妙之处也是答辩时可以主动讲给老师听的第一Transactional(rollbackFor Exception.class)保证步骤4和步骤5要么一起成功要么一起失败。如果第10个时段插入失败前9个时段不会持久化。第二数据库唯一索引承担了最终的并发安全防线。即使两个请求同时到达一个INSERT成功另一个INSERT抛出DuplicateKeyException被Spring捕获后事务回滚。第三生成订单号时加随机后缀是为了避免并发情况下订单号重复。实测在高并发压测下用JMeter开50个线程模拟同一时段抢购这套方案没有出现一单多卖或时段重复占用的情况。3.4 支付流程与回调微信支付V3的接入要点微信支付这块是毕设里最费时间的一环因为需要商户号、API证书、回调域名等一堆前置条件。我当时是跟着官方文档一步步配的说一下最关键的几个坑证书路径微信支付V3的商户私钥apiclient_key.pem需要放在服务器的固定位置我放在resources/cert/下代码里通过类路径加载。注意不要提交到GitHub公开仓库这是个大忌。回调接口下单后先调用微信的统一下单API拿到prepay_id返回给小程序端调wx.requestPayment。支付成功后微信服务器会异步通知你的回调接口这个接口必须返回正确格式的应答JSON格式 {code:SUCCESS}否则微信会连续重试造成重复通知。订单状态校验收到回调后先通过微信的API验签确认请求确实来自微信再更新订单状态。更新状态时必须加上WHERE status0条件防止重复回调导致状态错乱。如果你的毕设不打算接入真实支付因为需要企业资质可以做一个模拟支付小程序端点击“支付”按钮后弹窗提示“模拟支付成功”后端直接更新状态。但建议在论文里写明“为演示方便使用模拟支付生产环境需替换为微信支付V3”。4. 小程序端核心页面与逻辑实现原生开发的关键细节前面说了不推荐uni-app这里把对比讲透。原生开发的优势一是性能好小程序体积小、启动快尤其现在分包机制出来后原生体验更顺二是调试方便开发者工具里的“真机调试”对wxml和wxss的还原度高样式不会出现穿越性问题。缺点是写法相对SoC两个平台iOS和Android都要适配。但毕设只需要做微信小程序原生开发完全够用。4.1 底部TabBar与页面结构设计小程序端的页面结构我采用典型的“首页 预约 订单 我的”四Tab模式pages/index/index茶室列表展示封面、名称、地址、价格支持按距离排序。pages/booking/booking预约页面选择茶室后进入包含日期选择器、时段选择网格、总金额展示。pages/order/order订单列表切换Tab页签展示待支付/进行中/已完成/已取消。pages/mine/mine个人中心显示头像昵称、余额、优惠券扩展、客服电话。app.json中注册TabBar的代码{ pages: [ pages/index/index, pages/booking/booking, pages/order/order, pages/mine/mine ], tabBar: { list: [ {pagePath: pages/index/index, text: 首页, iconPath: images/home.png, selectedIconPath: images/home-active.png}, {pagePath: pages/booking/booking, text: 预约, iconPath: images/booking.png, selectedIconPath: images/booking-active.png}, {pagePath: pages/order/order, text: 订单, iconPath: images/order.png, selectedIconPath: images/order-active.png}, {pagePath: pages/mine/mine, text: 我的, iconPath: images/mine.png, selectedIconPath: images/mine-active.png} ] } }图标的尺寸官方要求是81px * 81px我用的png从iconfont-阿里巴巴矢量图标库下载换成纯色系现实效果很好。4.2 预约页面的时段选择交互这是小程序端最核心的交互逻辑。我用一个横向滚动的日期选择器展示未来7天 一个网格化的时段选择区域。需要注意的坑日期选择器小程序自带的picker组件modedate就够用了但设置start属性为今天、end属性为7天后需要动态计算。不能写死否则用户下个月打开会发现可选范围过了。const today new Date(); const end new Date(today); end.setDate(end.getDate() 7); this.setData({ dateStart: this.formatDate(today), dateEnd: this.formatDate(end), selectedDate: this.formatDate(today) });时段网格用flex-wrap布局每个格子渲染一个小时段。选中时改变背景色并累计总时长。关键点是页面加载时根据当前时间动态标记不可选时段请求后端接口拿到可预约状态async loadSlots() { const res await request.get(/api/room/slots, { roomId: this.data.roomId, date: this.data.selectedDate }); const slots res.data.map(slot { slot.selected false; return slot; }); this.setData({ slots }); }点击某个格子后判断是否已经选中如果是则取消否则选中并加入待预约列表。累计时长 选中的格子数 * 1小时总金额 时长 * 单价。这里有一个很实用的交互细节已经选中的格子不允许重复点击按钮置灰防止用户提交两个重复时段。之前见过一个还不错的方案是点选时段后弹窗让用户输入“人数”和“备注”把一次预约的数据收集全了再提交体验和答辩展示度都更完整。4.3 列表页的“加载更多”与防抖处理热词里有“微信小程序页面列表加载更多”说明这是个高频需求。小程序里做加载更多要处理三个问题上拉触底事件、节流防抖、loading状态管理。onReachBottom() { if (this.data.loading || this.data.noMore) return; // 防抖 this.loadMore(); }当用户上拉到底部时onReachBottom触发但需要判断当前是否正在请求中loadingtrue则直接return否则用户快速连续上拉会发起多个请求造成数据重复或乱序。分页参数pageNum从1开始pageSize固定10。每次加载更多pageNum自增接口返回的数据concat到原有list末尾。判断是否“没有更多了”的条件是当前这次返回的数据条数小于pageSize说明已经到底了。4.4 登录态管理与token刷新小程序端每次请求后端接口都要带上token存储在wx.setStorageSync(token, token)中。在request工具类里做统一封装const request (url, method, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL url, method: method || GET, data: data || {}, header: { Content-Type: application/json, Authorization: wx.getStorageSync(token) }, success: (res) { if (res.statusCode 401) { // token过期重新登录 wx.navigateTo({ url: /pages/login/login }); } else if (res.statusCode 200 res.data.code 0) { resolve(res.data); } else { wx.showToast({ title: res.data.msg, icon: none }); } }, fail: (err) { wx.showToast({ title: 网络异常, icon: none }); } }); }); };小程序不像Web端有cookie每次都要手动带header。我这里统一封装后页面里所有接口调用都走request维护起来很方便。5. 避坑实录从部署到答辩常见问题的一次性排查清单这部分是把我在毕设过程中踩过的坑、以及指导的学弟学妹们反复遇到的坑汇总一下。每一个都配有具体表现和解决方案建议你看到这里直接截图保存。5.1 微信开发者工具里的“合法域名”问题现象真机预览时请求后端接口报错“request:fail url not in domain list”。原因小程序要求所有网络请求的域名必须在微信公众平台的后台配置HTTPS白名单。本地开发用的是http://localhost:8080不在白名单内。解决开发者工具右上角“详情” → “本地设置” → 勾选“不校验合法域名、web-view业务域名、TLS版本以及HTTPS证书”。这是开发阶段的做法上线前必须在微信公众平台配置真实域名的HTTPS证书。5.2 SpringBoot MyBatis Plus的LocalDateTime类型映射现象Java 8的LocalDateTime字段插入数据库后变成“2024-05-20T10:30:00”格式的字符串前端解析出错。原因MyBatis默认的TypeHandler没对LocalDateTime做特殊处理序列化成带T的ISO格式。解决在application.yml里配置Jackson的日期格式统一为yyyy-MM-dd HH:mm:ssspring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8同时在MyBatis Plus的实体字段上加TableField注解指定jdbcType。或者更简单把数据库字段类型全部设为datetime实体字段用LocalDateTimeMyBatis Plus会自动映射。5.3 同一时段“幽灵预约”问题的终极排查现象两个用户同时提交同一茶室同一时段的预约都显示“预约成功”但实际上只有一个时段被占用。原因如果你用“先查room_slot是否存在再决定是否插入”的两步判断逻辑在高并发下会出现经典的条件竞争——两个请求都查到空闲然后都插入成功。更进一步如果你只是用了synchronized关键字锁Java代码多实例部署时锁不生效。解决正确做法是数据库唯一索引兜底前面已经详细说明了。插入失败时捕获DuplicateKeyException返回友好的错误提示“该时段已被预约请选择其他时段”。我再强调一遍毕设里数据库唯一索引是防超卖的最简单方案不要试图用代码层逻辑实现互斥那是分布式系统才需要考虑的。5.4 微信分享到朋友圈时小程序码的生成很多共享茶室需要做“分享给好友拼单”的营销功能这里涉及小程序码wxacode的生成。我踩过的坑是调用wxacode.getUnlimited接口时page参数不能带后缀问号否则报错“page参数不合法”。正确的做法是用scene参数传数据比如sceneroomId%3D12然后在页面onLoad里通过options.scene解析出参。5.5 时间处理时区与夏令时现象订单展示的时间比实际时间早了8小时。原因MySQL服务器的time_zone设置不对或Java应用时区和数据库时区不一致。一般建议在MySQL连接串里显式声明serverTimezoneAsia/Shanghaijdbc:mysql://localhost:3306/tea_room?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai5.6 管理后台的前端框架选择如果时间充裕管理后台可以单独做。选了Vue 3 Element Plus Vite页面包含“预约订单管理”和“茶室信息管理”。重点是接口鉴权和权限控制管理员登录后返回一个管理员token所有管理端请求校验这个token。这个做起来很快因为有现成的vue-element-admin模板可以改但答辩时被问到“你是怎么实现管理员权限的”就不能答“我套模板”——要知道JWT里可以放一个role字段后端每次校验时看role是否等于“ADMIN”。6. 性能优化与扩展思路给想要把毕设做成项目的同学如果你不满足于拿一个“合格”的分数想冲击“优秀”或者直接把项目商业化很多共享茶室老板真的会找你买系统下面这几个方向值得深耕。6.1 分布式锁在预约场景的进阶应用前面我说了数据库唯一索引是毕设方案的首选但如果你以后真的部署多实例可以用Redis的setnx实现分布式锁。预约时的加锁伪代码String lockKey room: roomId :date: date :hour: hour; boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, 30, TimeUnit.SECONDS); if (!locked) { throw new BusinessException(该时段正在被其他人抢占请稍后重试); } try { // 业务操作 } finally { redisTemplate.delete(lockKey); }注意锁超时时间要合理设置确保业务在锁过期前执行完成否则会出现锁提前释放导致安全漏洞。6.2 定时任务实现自动取消和自动完成预约状态机里提到了超时取消和自动完成这需要Spring的Scheduled注解定时扫描Component public class OrderScheduleTask { Scheduled(fixedRate 300000) // 每5分钟执行一次 public void autoCancelExpiredOrders() { // 找出所有待支付且创建时间超过15分钟的订单 // 更新状态为已取消并释放对应room_slot } Scheduled(fixedRate 60000) // 每1分钟执行一次 public void autoCompleteOrders() { // 找出所有已支付且end_time 当前时间的订单 // 更新状态为已完成 } }这里有个细节更新状态的同时要释放room_slot让该时段重新可以被预约如果有用户取消也同理。否则就会出现“订单已取消但时段永远被占用”的问题。处理取消订单时我建议用事务包裹先更新订单状态再删除或置空room_slot记录。6.3 消息通知公众号模板消息与短信用户预约成功或订单取消时给用户发通知是提升体验的重要手段。小程序里可以直接用微信的“订阅消息”功能。需要注意的坑是订阅消息必须用户主动授权且一次性订阅模板只能发一条消息长期订阅模板需要特殊权限。下单成功后弹窗提示用户“点击允许接收订单状态通知”否则后续无法推送。如果做短信通知用于超时提醒云短信服务商如阿里云一般需要企业资质才能审核通过模板个人开发者很难申请。所以毕设阶段建议只实现订阅消息。6.4 数据统计面板管理后台加一个统计面板非常加分。核心指标有每日订单数、各茶室到店率、用户复购率、高峰时段分布。这些数据全都可以从booking_order表和room_slot表里用SQL聚合出来。比如查某茶室最受欢迎的时段SELECT slot_hour, COUNT(*) AS cnt FROM room_slot WHERE room_id #{roomId} AND status 1 GROUP BY slot_hour ORDER BY cnt DESC LIMIT 5;统计面板用ECharts画折线图和柱状图答辩演示效果直接拉满。6.5 前后端分离的部署方案部署这块对毕设来说是锦上添花但确实能显示工程能力。我的推荐方案是服务器阿里云学生机ECS99元/年那个就够系统用CentOS 7。后端Spring Boot打成jar包通过systemd服务管理systemctl start tea-room反向代理用Nginx转发到8080端口。前端管理后台构建后放在Nginx的静态目录/usr/share/nginx/html小程序端不需要部署开发者工具上传后体验版即可。数据库MySQL直接装在服务器上注意开启远程访问bind-address0.0.0.0防火墙放行3306端口方便本地用Navicat连过去操作数据。Nginx的关键配置强制HTTPS的话还需要申请SSL证书server { listen 80; server_name yourdomain.com; location / { root /usr/share/nginx/html; index index.html; } location /api/ { proxy_pass http://localhost:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里一个小技巧把小程序端请求的BASE_URL配置为相对路径请求域名是https://yourdomain.com/api/xxx这样域名变了不用改小程序代码省去每次开发者工具上传前的手动替换。7. 答辩演示脚本让评委一眼看到你的工作量毕设差别最大的环节其实是答辩演示。代码写完了系统跑通了如果演示顺序不对评委可能十分钟内就看完了然后一个问题都没得问这对你来说反而是浪费——答辩的时间越长通常说明评委对你感兴趣准备的问题你答上来分数自然高。我整理了一份演示顺序清单照着做基本稳背景与痛点30秒共享茶室无人值守传统电话预约效率低、超卖严重需要一个在线预约小程序。系统架构1分钟画一张简易架构图不用太细节画到Spring Boot MySQL 微信小程序三层就够讲清楚数据流向。用户端演示3分钟小程序登录 → 选茶室 → 选时段 → 提交订单 → 模拟支付 → 在订单列表看到新订单状态变化。商家端演示2分钟登录管理后台 → 查看订单 → 手动确认订单完成 → 新增一个茶室 → 修改茶室价格。并发演示1分钟这一步最加分。打开两个浏览器窗口或两个手机同时提交同一茶室同一时段的预约展示第二个请求被拒绝的友好提示然后展示数据库room_slot表里只有一条记录。这个演示能直接把你的“防超卖设计”可视化评委想不记住都难。还有一个答辩必问的问题“你这个系统有什么不足”千万别回答“没有不足”。我的标准答案是当前系统的预约粒度是小时级未来的共享茶室可能按半小时甚至分钟级计费这需要对时段表做更细的粒度划分当前支付是模拟的接入真实微信支付需要企业资质和服务器备案这也是商业化的下一步。最后再送你一条经验写代码之前先写文档文档写清楚再敲键盘。我当时是先画好ER图再写用户故事user story再列接口清单最后才写代码。这个过程看起来慢实际省了大量返工时间。系统的核心是需求分析而需求分析的成果就是那几张表和状态机。你把这个想明白了剩下的代码不过是体力活。