首页 / 资讯中心 / 文章详情

停车场预约小程序毕设:数据模型、并发锁与超时释放全解析

停车场预约小程序毕设:数据模型、并发锁与超时释放全解析 ★ FEATURED ARTICLE
简介这是一套面向计算机相关专业在校学生与教师的微信小程序停车场预约管理系统完整项目可作为毕业设计、课程设计或大作业的参考方案。项目代码经过验证可稳定运行涵盖小程序端与管理后台适合入门进阶学习也支持在此基础上二次开发扩展功能。压缩包共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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 车位表 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 预约订单表核心 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段 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组件的modedate时段用自定义的横向滚动标签每个标签代表一小时。用户选中日期和时段后调用后端接口查询该时段剩余车位。// 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); ListReservationOrder 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 MapOrderStatus, SetOrderStatus 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 秒刷新一次结果用户看到有余位、点进去却预约失败体验极差。后来改成实时查询虽然数据库压力大一点但毕设场景下完全扛得住。宁可慢一点也不要给用户假信息。这个习惯我一直保持到现在希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站