☰
共享茶室预约系统实战:Spring Boot + 微信小程序从设计到部署
2026/10/5 10:47:10 网站建设 项目流程

很多做计算机毕设的同学看到“共享茶室预约”这类题目,第一反应是“不就是个增删改查吗”。真动手之后才发现,微信小程序端、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的同学来说,学习曲线比SSH(Struts+Spring+Hibernate)时代平滑太多。
  • 持久层:MyBatis Plus。我选它不是因为MyBatis不好,而是因为单表CRUD场景下MyBatis Plus的BaseMapper直接帮你把insert、update、selectById这些都写好了,你能把精力省下来放在预约冲突、订单状态流转这些真正有含金量的逻辑上。数据量就几千条,不存在性能瓶颈,这点可以放心用。
  • 数据库:MySQL 8.0+,Navicat做可视化操作。为什么不用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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

表结构本身中规中矩,但有两个细节我得提醒你:

第一,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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这张表的关键是营业时段与预约时段的取舍。如果不限定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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

status字段是这张表的灵魂。我把订单状态定义为一个“有限状态机”,每个状态都有明确的迁移条件:

  • 待支付(0):用户提交预约订单但未付款。为了防止恶意占座,待支付订单只保留15分钟,超时自动取消并释放时段。
  • 已支付(1):支付成功后进入该状态,茶室时段锁定不可再被预约。
  • 已完成(2):使用时间结束后,系统自动或用户手动确认完成。这里我加了一个定时任务,每5分钟扫描一次,将end_time小于当前时间的“已支付”订单自动置为“已完成”,防止用户忘记确认导致状态卡死。
  • 已取消(3):用户在预约开始前取消,或超时未支付自动取消。
  • 已退款(4):取消后原路退回款项,或者管理员后台手动退款。

这里有一个实战经验:状态迁移必须做幂等保护。比如你接了支付回调,万一微信服务器因为网络原因多次回调你的接口,代码里没做状态判断就会把订单从“已支付”改到“已完成”再改到“已退款”,直接炸掉。所以每次更新状态前先查一次当前状态,只有符合迁移条件的才允许更新(用UPDATE语句的WHERE条件去控制,比如UPDATE booking_order SET status=1 WHERE id=? AND status=0)。

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 '占用的订单ID,NULL表示空闲', `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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这张表是解决高并发下订单冲突问题的核心手段。共享茶室和电影院很像:同一个房间的同一个小时段,只能被一个订单占用。所以我在room_slot上建立了唯一索引uk_room_date_hour,数据库层面强约束。

用户提交订单时,后端做三步操作:

  1. 遍历用户选择的所有小时段,逐条插入room_slot,设置了order_id为本次订单号;
  2. 如果任何一条插入失败(违反唯一索引),说明该时段已被别人抢走,抛出异常,回滚前面所有成功的插入;
  3. 全部插入成功后再生成订单。

配合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_type=authorization_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有效期不固定,且只在特定时候能用。我统一用JWT(JSON Web Token)做鉴权,后端写一个拦截器,每次请求从Header里取出token,校验合法后把userId放入ThreadLocal供后续业务使用。

JWT依赖引入:

<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>

3.2 获取可预约时段:动态计算而不是硬编码

小程序端需要展示“今天能约哪些小时”,这个接口看起来简单,其实有讲究。你要根据当前时间动态过滤掉已过去的时段:

public List<HourSlot> 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(); List<HourSlot> 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, List<Integer> 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 status=0条件,防止重复回调导致状态错乱。

如果你的毕设不打算接入真实支付(因为需要企业资质),可以做一个模拟支付:小程序端点击“支付”按钮后,弹窗提示“模拟支付成功”,后端直接更新状态。但建议在论文里写明“为演示方便,使用模拟支付,生产环境需替换为微信支付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组件mode="date"就够用了,但设置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触发,但需要判断当前是否正在请求中(loading=true则直接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:ss:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

同时在MyBatis Plus的实体字段上加@TableField注解,指定jdbcType。或者更简单:把数据库字段类型全部设为datetime,实体字段用LocalDateTime,MyBatis Plus会自动映射。

5.3 同一时段“幽灵预约”问题的终极排查

现象:两个用户同时提交同一茶室同一时段的预约,都显示“预约成功”,但实际上只有一个时段被占用。

原因:如果你用“先查room_slot是否存在,再决定是否插入”的两步判断逻辑,在高并发下会出现经典的条件竞争——两个请求都查到空闲,然后都插入成功。更进一步,如果你只是用了synchronized关键字锁Java代码,多实例部署时锁不生效。

解决:正确做法是数据库唯一索引兜底(前面已经详细说明了)。插入失败时捕获DuplicateKeyException,返回友好的错误提示“该时段已被预约,请选择其他时段”。我再强调一遍:毕设里数据库唯一索引是防超卖的最简单方案,不要试图用代码层逻辑实现互斥,那是分布式系统才需要考虑的。

5.4 微信分享到朋友圈时小程序码的生成

很多共享茶室需要做“分享给好友拼单”的营销功能,这里涉及小程序码(wxacode)的生成。我踩过的坑是:调用wxacode.getUnlimited接口时,page参数不能带后缀问号,否则报错“page参数不合法”。正确的做法是用scene参数传数据,比如scene=roomId%3D12,然后在页面onLoad里通过options.scene解析出参。

5.5 时间处理:时区与夏令时

现象:订单展示的时间比实际时间早了8小时。

原因:MySQL服务器的time_zone设置不对,或Java应用时区和数据库时区不一致。一般建议在MySQL连接串里显式声明serverTimezone=Asia/Shanghai:

jdbc:mysql://localhost:3306/tea_room?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai

5.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 前后端分离的部署方案

部署这块对毕设来说是锦上添花,但确实能显示工程能力。我的推荐方案是:

  • 服务器:阿里云学生机ECS(99元/年那个就够),系统用CentOS 7。
  • 后端:Spring Boot打成jar包,通过systemd服务管理(systemctl start tea-room),反向代理用Nginx转发到8080端口。
  • 前端:管理后台构建后放在Nginx的静态目录(/usr/share/nginx/html),小程序端不需要部署,开发者工具上传后体验版即可。
  • 数据库:MySQL直接装在服务器上,注意开启远程访问(bind-address=0.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. 答辩演示脚本:让评委一眼看到你的工作量

毕设差别最大的环节其实是答辩演示。代码写完了,系统跑通了,如果演示顺序不对,评委可能十分钟内就看完了,然后一个问题都没得问,这对你来说反而是浪费——答辩的时间越长,通常说明评委对你感兴趣,准备的问题你答上来,分数自然高。

我整理了一份演示顺序清单,照着做基本稳:

  1. 背景与痛点(30秒):共享茶室无人值守,传统电话预约效率低、超卖严重,需要一个在线预约小程序。
  2. 系统架构(1分钟):画一张简易架构图(不用太细节,画到Spring Boot + MySQL + 微信小程序三层就够),讲清楚数据流向。
  3. 用户端演示(3分钟):小程序登录 → 选茶室 → 选时段 → 提交订单 → 模拟支付 → 在订单列表看到新订单状态变化。
  4. 商家端演示(2分钟):登录管理后台 → 查看订单 → 手动确认订单完成 → 新增一个茶室 → 修改茶室价格。
  5. 并发演示(1分钟):这一步最加分。打开两个浏览器窗口或两个手机,同时提交同一茶室同一时段的预约,展示第二个请求被拒绝的友好提示,然后展示数据库room_slot表里只有一条记录。这个演示能直接把你的“防超卖设计”可视化,评委想不记住都难。

还有一个答辩必问的问题:“你这个系统有什么不足?”千万别回答“没有不足”。我的标准答案是:当前系统的预约粒度是小时级,未来的共享茶室可能按半小时甚至分钟级计费,这需要对时段表做更细的粒度划分;当前支付是模拟的,接入真实微信支付需要企业资质和服务器备案,这也是商业化的下一步。

最后再送你一条经验:写代码之前先写文档,文档写清楚再敲键盘。我当时是先画好ER图,再写用户故事(user story),再列接口清单,最后才写代码。这个过程看起来慢,实际省了大量返工时间。系统的核心是需求分析,而需求分析的成果就是那几张表和状态机。你把这个想明白了,剩下的代码不过是体力活。

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

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

立即咨询