☰
停车场预约小程序毕设:数据模型、并发锁与超时释放全解析
2026/10/22 10:39:42 网站建设 项目流程

简介:这是一套面向计算机相关专业在校学生与教师的微信小程序停车场预约管理系统完整项目,可作为毕业设计、课程设计或大作业的参考方案。项目代码经过验证可稳定运行,涵盖小程序端与管理后台,适合入门进阶学习,也支持在此基础上二次开发扩展功能。压缩包共1126个文件,约30.58MB,包含154个js脚本、112个java后端源码、110个vue前端组件,以及69个wxss与67个wxml小程序页面文件,另有2个sql数据库脚本、3个mp4运行演示视频和配套图片资源,结构完整、层次清晰。目前已有254人学习下载。读者可获得完整源码、数据库文件与运行录屏,对照演示快速理解预约流程、车位管理与后台交互逻辑,并借助现成目录结构排查环境配置问题,降低搭建与调试成本。

1. 停车场预约小程序:一个被低估的毕设选题,到底能做出什么

很多同学选毕设题目时,第一反应是避开“管理系统”四个字,觉得烂大街、没技术含量、答辩时容易被老师一句“这不就是个增删改查吗”问得哑口无言。但停车场预约这个方向恰恰是个例外——它表面上是管理系统,骨子里却藏着一套实时状态同步、时段冲突检测、订单超时释放的逻辑,比单纯的图书管理、学生成绩管理要硬核得多。我见过不少开发者在这个题目上翻车,不是因为功能写不出来,而是因为把“预约”当成了普通的表单提交,忽略了车位作为稀缺资源在时间维度上的独占性。

这个选题适合两类人:一是正在找毕设方向、希望工作量适中但又有亮点可讲的同学;二是想练手微信小程序全栈开发、需要一个完整业务闭环的开发者。它涉及的技术栈覆盖小程序前端、后端接口、数据库设计、定时任务、支付模拟等环节,麻雀虽小五脏俱全。你做完这一套,对“预约类系统”的通用套路基本就通了,换成会议室预约、球场预约、自习室占座,逻辑几乎可以直接迁移。

2. 从车位到订单:停车场预约系统的数据模型怎么设计

2.1 为什么不能把车位当成普通商品来建模

普通电商系统的商品是“可无限复制”的,你买一件库存减一,但同一件商品可以卖给无数人。车位不一样——一个车位在某个时间段内只能被一辆车占用,这是硬约束。如果你用商品表的思路去做,把车位当成库存字段,那就会出现“同一时段卖出两个预约”的超卖问题。更麻烦的是,停车预约还有“入场”和“离场”两个时间点,中间可能提前走、可能超时停,这些状态变化都要在数据模型里体现出来。

我一般会把核心实体拆成四张表:停车场表、车位表、预约订单表、车辆信息表。停车场表记录名称、地址、总车位数、营业时间、计费规则;车位表记录所属停车场、车位编号、类型(普通/充电/无障碍)、当前状态;预约订单表是核心,记录用户、车位、预约时间段、实际入场时间、实际离场时间、订单状态、费用;车辆信息表记录车牌号与用户的绑定关系。

这里有个关键设计决策:车位状态到底存不存?有些方案为了查询快,在车位表里放一个 status 字段,值为“空闲/已预约/占用”。但这个字段在预约场景下很容易变成脏数据——用户预约了但没来,车位显示“已预约”,实际空着;用户提前走了,车位显示“占用”,实际空着。我的做法是车位表只存物理属性,状态完全由预约订单表实时计算。查询某个时间段内某停车场剩余车位时,用时间区间重叠判断来算,而不是读状态字段。

2.2 建表 SQL 与时间区间重叠判断

下面是我常用的建表语句,以 MySQL 为例,字段类型和索引都经过实际项目验证:

-- 停车场表 CREATE TABLE parking_lot ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT '停车场名称', address VARCHAR(255) NOT NULL COMMENT '地址', total_spots INT NOT NULL DEFAULT 0 COMMENT '总车位数量', open_time TIME NOT NULL DEFAULT '00:00:00' COMMENT '营业开始时间', close_time TIME NOT NULL DEFAULT '23:59:59' COMMENT '营业结束时间', hourly_rate DECIMAL(10,2) NOT NULL DEFAULT 5.00 COMMENT '每小时费率', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 车位表 CREATE TABLE parking_spot ( id INT PRIMARY KEY AUTO_INCREMENT, lot_id INT NOT NULL COMMENT '所属停车场ID', spot_no VARCHAR(20) NOT NULL COMMENT '车位编号,如A-001', spot_type TINYINT NOT NULL DEFAULT 1 COMMENT '1普通 2充电 3无障碍', is_active TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用', UNIQUE KEY uk_lot_spot (lot_id, spot_no), INDEX idx_lot (lot_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 预约订单表(核心) CREATE TABLE reservation_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '订单号', user_id INT NOT NULL COMMENT '用户ID', lot_id INT NOT NULL COMMENT '停车场ID', spot_id INT NOT NULL COMMENT '车位ID', plate_no VARCHAR(10) NOT NULL COMMENT '车牌号', start_time DATETIME NOT NULL COMMENT '预约开始时间', end_time DATETIME NOT NULL COMMENT '预约结束时间', actual_entry DATETIME DEFAULT NULL COMMENT '实际入场时间', actual_exit DATETIME DEFAULT NULL COMMENT '实际离场时间', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待入场 1已入场 2已完成 3已取消 4已超时', total_fee DECIMAL(10,2) DEFAULT 0.00 COMMENT '总费用', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_spot_time (spot_id, start_time, end_time), INDEX idx_user (user_id), INDEX idx_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这段 SQL 里最值得说的是idx_spot_time这个联合索引。预约系统最高频的查询是“某个车位在某个时间段内是否已被占用”,条件形如spot_id = ? AND start_time < ? AND end_time > ?。把 spot_id 放在最左边,时间字段跟在后面,能走索引范围扫描,比全表扫描快几个数量级。另外订单状态用 TINYINT 而不是 ENUM,是因为 ENUM 在后续加状态时要改表结构,TINYINT 配合代码里的常量更灵活。

时间区间重叠的判断条件是整个系统的逻辑基石,写成 SQL 就是:

-- 检查某车位在指定时间段内是否已被预约 SELECT COUNT(*) FROM reservation_order WHERE spot_id = ? AND status IN (0, 1) -- 待入场或已入场才算占用 AND start_time < ? -- 新预约的结束时间 AND end_time > ?; -- 新预约的开始时间

这个条件的意思是:已有订单的开始时间早于新订单的结束时间,且已有订单的结束时间晚于新订单的开始时间,两者就重叠了。注意边界情况——如果已有订单结束时间正好等于新订单开始时间,不算重叠,这是合理的,因为前一辆车刚好开走。参数说明:第一个?是新预约的 end_time,第二个?是新预约的 start_time,顺序别搞反,我见过有人写反导致永远查不到冲突。

2.3 订单状态流转与超时释放

订单状态不是随便改的,必须按流转图走:待入场 → 已入场 → 已完成;待入场 → 已取消;待入场 → 已超时。已入场的订单不能直接取消,必须先离场结算。已完成的订单不能回退。这些约束要写在业务层,不能只靠前端按钮控制。

超时释放是停车场预约区别于普通预约的关键。用户预约了 14:00 入场,但 14:30 还没来,车位应该释放出来给别人用。常见做法是用定时任务扫描待入场订单,把start_time + 宽限期 < 当前时间的订单状态改为已超时。宽限期一般设 15 到 30 分钟,太短用户体验差,太长车位周转率低。定时任务可以用 Spring 的@Scheduled或者 Node.js 的node-cron,频率不用太高,每分钟扫一次足够。

3. 微信小程序端:预约流程的页面与交互实现

3.1 页面结构规划与 tabBar 配置

小程序端的页面不用多,五个就够:首页(停车场列表)、停车场详情(车位选择与时段选择)、订单确认、我的订单、个人中心。tabBar 放三个:首页、订单、我的。停车场详情页是核心,要同时展示车位布局和时段选择器。

app.json的配置如下:

{ "pages": [ "pages/index/index", "pages/lot-detail/detail", "pages/order-confirm/confirm", "pages/my-orders/orders", "pages/profile/profile" ], "tabBar": { "list": [ { "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "pages/my-orders/orders", "text": "订单" }, { "pagePath": "pages/profile/profile", "text": "我的" } ] }, "window": { "navigationBarTitleText": "停车场预约", "navigationBarBackgroundColor": "#2c3e50" } }

tabBar 的图标文件需要放在images目录下,尺寸建议 81x81 像素,太大影响加载速度。页面路径必须和实际文件目录一致,否则编译报错。这里没有用自定义 tabBar,因为毕设场景下原生 tabBar 足够,自定义反而增加调试成本。

3.2 时段选择器与实时余位查询

停车场详情页需要两个核心交互:选日期、选时段。日期用picker组件的mode="date",时段用自定义的横向滚动标签,每个标签代表一小时。用户选中日期和时段后,调用后端接口查询该时段剩余车位。

// pages/lot-detail/detail.js Page({ data: { lotId: null, date: '', timeSlots: [], // 生成的时段列表 selectedSlot: -1, // 当前选中的时段索引 availableCount: 0 // 剩余车位 }, onLoad(options) { this.setData({ lotId: options.lotId }); this.generateTimeSlots(); }, // 根据停车场营业时间生成时段标签 generateTimeSlots() { const slots = []; for (let h = 8; h < 22; h++) { slots.push({ label: `${h}:00-${h + 1}:00`, start: `${h}:00`, end: `${h + 1}:00` }); } this.setData({ timeSlots: slots }); }, // 用户点击某个时段 onSlotTap(e) { const index = e.currentTarget.dataset.index; this.setData({ selectedSlot: index }); this.queryAvailable(index); }, // 查询该时段剩余车位 queryAvailable(index) { const slot = this.data.timeSlots[index]; wx.request({ url: 'https://your-api.com/api/spot/available', method: 'GET', data: { lotId: this.data.lotId, date: this.data.date, startTime: slot.start, endTime: slot.end }, success: (res) => { if (res.data.code === 0) { this.setData({ availableCount: res.data.data.count }); } } }); } });

这段代码里generateTimeSlots写死了 8 点到 22 点,实际项目应该从停车场详情接口读取open_time和close_time来动态生成。queryAvailable里的 URL 要换成你自己的后端地址,微信开发者工具里需要在“详情-本地设置”勾选“不校验合法域名”才能调试本地接口。availableCount返回后要在页面上显示“剩余 X 个车位”,如果为 0 则禁用确认按钮。

3.3 下单接口调用与支付模拟

确认订单页需要收集车牌号、预约时段,然后调用下单接口。微信支付在毕设里通常不接真实商户号,用模拟支付即可——点击“支付”后直接调后端接口把订单状态改为已支付(待入场),弹一个wx.showToast提示成功。

// pages/order-confirm/confirm.js Page({ data: { lotId: null, spotId: null, plateNo: '', startTime: '', endTime: '' }, onLoad(options) { // 从详情页传过来的参数 this.setData({ lotId: options.lotId, spotId: options.spotId, startTime: options.start, endTime: options.end }); }, // 车牌号输入 onPlateInput(e) { this.setData({ plateNo: e.detail.value.toUpperCase() }); }, // 提交订单 submitOrder() { const { lotId, spotId, plateNo, startTime, endTime } = this.data; if (!plateNo || plateNo.length < 7) { wx.showToast({ title: '请输入正确车牌号', icon: 'none' }); return; } wx.request({ url: 'https://your-api.com/api/order/create', method: 'POST', header: { 'content-type': 'application/json' }, data: { lotId, spotId, plateNo, startTime: `${this.data.date} ${startTime}:00`, endTime: `${this.data.date} ${endTime}:00` }, success: (res) => { if (res.data.code === 0) { // 模拟支付成功 wx.showToast({ title: '预约成功', icon: 'success' }); setTimeout(() => { wx.switchTab({ url: '/pages/my-orders/orders' }); }, 1500); } else { wx.showToast({ title: res.data.msg, icon: 'none' }); } } }); } });

车牌号输入用toUpperCase()统一转大写,避免用户输入小写导致查询不一致。下单接口的startTime和endTime要拼上日期,格式为YYYY-MM-DD HH:mm:ss,后端解析时注意时区问题。模拟支付这里没有调wx.requestPayment,因为那需要真实商户配置,毕设答辩时老师一般不会深究支付环节,重点看预约逻辑是否闭环。

4. 后端接口与定时任务:让车位真正“活”起来

4.1 预约接口的并发冲突处理

预约接口最怕的是并发——两个用户同时看到最后一个车位,同时点确认,如果处理不当就会超卖。常见做法是在插入订单前加一道数据库层面的校验:用SELECT ... FOR UPDATE锁住该车位的相关订单行,或者用唯一索引兜底。

我一般会在订单表上加一个联合唯一索引,把spot_id、start_time、end_time和status组合起来,但 status 会变,不适合做唯一键。更稳妥的方案是用 Redis 分布式锁,key 为lock:spot:{spotId}:{date},过期时间 5 秒。抢到锁的请求才去查冲突、插订单,没抢到的直接返回“手慢了”。

// 伪代码:预约核心逻辑 public Result createOrder(CreateOrderDTO dto) { String lockKey = "lock:spot:" + dto.getSpotId() + ":" + dto.getDate(); // 尝试获取分布式锁,等待3秒,持有5秒 boolean locked = redisLock.tryLock(lockKey, 3, 5, TimeUnit.SECONDS); if (!locked) { return Result.fail("当前预约人数较多,请稍后重试"); } try { // 查冲突 int conflict = orderMapper.countConflict( dto.getSpotId(), dto.getStartTime(), dto.getEndTime()); if (conflict > 0) { return Result.fail("该时段车位已被预约"); } // 插入订单 ReservationOrder order = new ReservationOrder(); // ... 填充字段 orderMapper.insert(order); return Result.ok(order.getOrderNo()); } finally { redisLock.unlock(lockKey); } }

tryLock的参数含义:第一个是 key,第二个是最大等待时间(秒),第三个是锁自动释放时间(秒)。锁自动释放是为了防止某个请求拿到锁后宕机导致死锁。countConflict对应的 SQL 就是前面写的重叠判断。这段逻辑虽然简单,但少了锁或者少了冲突检查,线上就会出问题。

4.2 超时订单自动释放的定时任务

定时任务用 Spring 的@Scheduled注解实现,每分钟执行一次:

@Component public class OrderTimeoutTask { @Autowired private ReservationOrderMapper orderMapper; // 每分钟执行一次,释放超时未入场的订单 @Scheduled(cron = "0 * * * * ?") public void releaseTimeoutOrders() { // 宽限期30分钟 Date deadline = new Date(System.currentTimeMillis() - 30 * 60 * 1000); List<ReservationOrder> timeoutOrders = orderMapper.selectTimeoutOrders(deadline); for (ReservationOrder order : timeoutOrders) { order.setStatus(4); // 已超时 orderMapper.updateById(order); // 可选:发模板消息通知用户 } if (!timeoutOrders.isEmpty()) { log.info("释放超时订单 {} 条", timeoutOrders.size()); } } }

cron表达式0 * * * * ?表示每分钟的第 0 秒触发。selectTimeoutOrders的查询条件是status = 0 AND start_time < deadline。宽限期 30 分钟是经验值,你可以根据业务调整。注意定时任务在集群环境下会重复执行,毕设单机部署不用考虑,但如果要写进论文的“扩展性”章节,可以提一句用 Redis 锁或 Quartz 集群方案。

4.3 离场结算与费用计算

用户实际离场时,需要计算停车费用。计费规则一般是:首小时 X 元,之后每小时 Y 元,不足一小时按一小时算。如果实际离场时间超过预约结束时间,超出部分按标准费率计费;如果提前离场,按实际时长计费。

public BigDecimal calculateFee(ReservationOrder order, Date actualExit) { long duration = actualExit.getTime() - order.getActualEntry().getTime(); long hours = (long) Math.ceil(duration / (60.0 * 60 * 1000)); // 向上取整 if (hours <= 1) { return order.getHourlyRate(); // 首小时 } // 首小时 + 超出部分 return order.getHourlyRate() .add(order.getHourlyRate().multiply(BigDecimal.valueOf(hours - 1))); }

Math.ceil保证不足一小时按一小时算。hourlyRate从停车场表读取,不要写死在代码里。实际项目中还要考虑免费时长、每日封顶等规则,毕设阶段实现基础版即可,论文里可以写“预留了费率策略接口,便于扩展”。

5. 避坑指南:停车场预约系统开发中最容易翻车的五个地方

现象一:用户预约成功但车位显示仍为空闲。原因:车位状态字段没有及时更新,或者查询余位时没有把待入场订单算进去。解决:放弃车位状态字段,余位查询统一走订单表的时间重叠判断,确保数据源唯一。

现象二:两个用户同时预约同一车位同一时段,都成功了。原因:没有加锁或没有做冲突检查,并发请求同时通过了校验。解决:加 Redis 分布式锁,锁的粒度到车位+日期,同时在数据库层用SELECT ... FOR UPDATE兜底。

现象三:定时任务把已入场的订单也释放了。原因:查询超时订单时条件写成了start_time < deadline,没有限定status = 0。解决:查询条件必须同时满足status = 0 AND start_time < deadline,已入场的订单不受超时释放影响。

现象四:小程序端时间选择器传的时间格式后端解析失败。原因:前端传的是HH:mm,后端期望YYYY-MM-DD HH:mm:ss。解决:在submitOrder里拼接完整日期,或者后端做兼容解析。我一般在前端拼好,减少后端歧义。

现象五:车牌号输入带空格或小写,导致查询不到订单。原因:用户输入习惯不一致。解决:前端trim()去空格并转大写,后端入库前也做一次标准化处理,双重保险。

6. 进阶技巧:用状态机管住订单流转,用压测验证并发安全

订单状态流转如果只靠 if-else 判断,代码会越来越乱,加一个状态就得到处改。我习惯用一个简单的状态机来管:定义允许的流转路径,每次变更前校验。

public enum OrderStatus { PENDING(0), ENTERED(1), COMPLETED(2), CANCELLED(3), TIMEOUT(4); private final int code; // 定义允许的流转 private static final Map<OrderStatus, Set<OrderStatus>> TRANSITIONS = Map.of( PENDING, Set.of(ENTERED, CANCELLED, TIMEOUT), ENTERED, Set.of(COMPLETED), COMPLETED, Set.of(), CANCELLED, Set.of(), TIMEOUT, Set.of() ); public boolean canTransitionTo(OrderStatus target) { return TRANSITIONS.getOrDefault(this, Set.of()).contains(target); } }

变更状态时先调canTransitionTo,不合法就抛异常。这样即使前端传了错误的操作,后端也能拦住。这个状态机不到 20 行代码,但能让你的答辩 PPT 多一个“设计模式”亮点。

验证并发安全最直接的办法是压测。用 JMeter 或 wrk 对预约接口发 100 个并发请求,抢同一个车位同一时段,看最终成功了几单。正确结果是只有 1 单成功,其余返回“已被预约”。如果成功超过 1 单,说明锁没生效或冲突检查有漏洞。压测时注意把 Redis 锁的等待时间调短,否则大量请求排队会拖慢响应。

最后说一个我踩过的坑:早期版本我把余位查询做成了缓存,每 10 秒刷新一次,结果用户看到有余位、点进去却预约失败,体验极差。后来改成实时查询,虽然数据库压力大一点,但毕设场景下完全扛得住。宁可慢一点,也不要给用户假信息。这个习惯我一直保持到现在,希望帮到你。

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

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

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

立即咨询