拓冰建站拓冰建站
首页 / 资讯中心 / 正文

微信小程序车位预约系统:并发控制与状态机设计实战

简介基于微信小程序的车位预约系统设计与实施完整覆盖需求分析、系统架构、数据库设计、前后端功能实现及测试等环节适合高校学生用于毕业设计/课程设计参考也适合初学小程序开发的开发者学习完整项目流程。压缩包共含8个文件包括说明文档doc、答辩演示pptx、源码与演示视频rar以及数据库sql脚本整体大小约45.09MB。文档中给出了系统架构设计、开发流程设计、实体ER图、数据表结构、管理员与用户端功能实现、测试方案与结果分析等具体内容。已有459人学习下载通过这套资料可快速了解车位预约系统的模块划分并能对照演示视频和SQL脚本梳理前后端联调与数据表设计思路。1. 微信小程序车位预约为什么它是课程设计里的常青树如果你正在找 Java 课程设计案例源码或者微信小程序项目实例车位预约系统几乎是绕不开的一个选题。原因很简单它麻雀虽小五脏俱全——有用户登录、有车位状态展示、有预约下单、有订单状态流转甚至还牵扯到并发抢车位的场景。一个小程序前端加一个后端服务就能把移动端开发、接口设计、数据库建模、状态机管理这些硬技能全串起来。这套系统的核心诉求很直接用户打开小程序看到当前哪些车位空闲、哪些被占用选中一个车位发起预约到场后确认或取消。管理员侧则要能看到车位利用率、订单列表甚至手动释放异常占用的车位。它不像电商项目那样要堆商品、购物车、优惠券也不像社交项目那样要处理复杂的实时通信业务边界清晰最适合拿来练手和答辩。但“简单”不等于“好做”。我见过太多人卡在同一个地方预约成功了车位却没锁住管理员释放了车位用户那边还显示占用数据库时间乱了明明约的是明天系统当成今天处理。这些问题不是代码量的问题是设计阶段没把状态流转和并发边界想清楚。这篇笔记就按“设计 → 建表 → 小程序端 → 后端核心逻辑 → 坑”的顺序给你一条能直接复现的路径。2. 需求拆解与数据库设计先把车位的状态流转画清楚2.1 角色与核心流程用户、管理员、车位三者的关系做这类系统第一步不是写代码是画角色和状态图。常见做法是拆成三类角色普通用户小程序端、管理员通常是后台管理页面或者直接操作数据库、以及系统本身定时任务。用户侧的流程最多五步登录 → 查看车位 → 发起预约 → 到场/取消 → 完成。管理员侧的流程更简单查看总览 → 处理异常订单 → 管理车位。这里最容易翻车的地方是“预约”和“占用”的边界。很多初稿把车位状态设计成“空闲/占用”两个值结果用户预约了 18:00-19:00 的车位但 17:30 就显示占用其他人没法预约 19:00 之后的时间。问题就出在状态没有跟订单绑定车位本身也不该只存一个静态状态。正确的做法是给车位加一个“当前状态”字段用于展示同时用订单的时间段来约束可用性判断。展示用状态判断用时间两者配合而不是混用。2.2 建表语句车位表、订单表、用户表的字段取舍直接给一套我常用的建表方案MySQL 8 起步。两张核心表parking_space 和 reservation_order再加一张 user 表存微信用户信息。-- 车位表 CREATE TABLE parking_space ( id bigint NOT NULL AUTO_INCREMENT, space_no varchar(16) NOT NULL COMMENT 车位编号如 A-01, location varchar(64) DEFAULT NULL COMMENT 车位区域描述, status tinyint NOT NULL DEFAULT 0 COMMENT 0-空闲 1-占用 2-维护中, version int NOT NULL DEFAULT 0 COMMENT 乐观锁版本号用于并发控制, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_space_no (space_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车位信息表;-- 预约订单表 CREATE TABLE reservation_order ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 业务订单号, user_id bigint NOT NULL COMMENT 用户ID, space_id bigint NOT NULL COMMENT 车位ID, start_time datetime NOT NULL COMMENT 预约开始时间, end_time datetime NOT NULL COMMENT 预约结束时间, status tinyint NOT NULL DEFAULT 0 COMMENT 0-待生效 1-生效中 2-已完成 3-已取消 4-超时未到, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_space_id (space_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约订单表;这里的设计要点有三个。第一车位表的 status 是冗余字段真正判断一个车位能否预约要看订单表里有没有时间重叠的未取消订单。第二version 字段是留给乐观锁用的预约时检查 version更新时比对 version防止两个人同时约到同一个车位。第三order_no 建议用“日期随机数”或雪花 ID 生成不要用自增主键直接对外展示否则订单号会泄露系统每天的订单量。2.3 时间字段的隐藏坑时区、CURRENT_TIMESTAMP 与业务时间的矛盾数据库时间坑是新手最容易踩的而且一踩一个准。MySQL 的 datetime 类型不带时区信息如果你的服务器设置了东八区JDBC 连接串也指定了 serverTimezoneAsia/Shanghai那正常。但如果你把数据库部署在 Docker 容器里容器默认是 UTC 时区你又没在连接串里显式声明Java 程序读出来的时间就比北京时间差了 8 小时。我的习惯是两条铁律。第一连接串里永远显式加 serverTimezoneAsia/Shanghai 和 useLegacyDatetimeCodefalse。第二所有时间字段在 Java 实体类里用 LocalDateTime不碰 java.util.Date。第一条解决时区错乱第二条解决存取时的隐式转换问题。另外预约业务里建议把时间存成 datetime 而不是 timestamptimestamp 有 2038 年问题虽然对课程设计无所谓但养成习惯没坏处。3. 小程序端设计与实现从登录到预约成功的完整链路3.1 页面结构设计四个页面撑起全部业务小程序端的页面不用多四个就够首页车位列表和状态总览、预约页选择时间、确认车位、订单页我的预约列表、个人页登录状态和基本信息。如果你要加管理员功能另做一个小程序端或者用 Web 管理后台都行别塞在一起。首页是门面建议用 canvas 或 view 画一个简单的车位分布图绿色代表空闲红色代表占用灰色代表维护中。不要用图片直接拿 view 加背景色画格子性能和可维护性都好得多。点击空闲车位跳到预约页传入车位 ID 和车位编号。预约页的核心是一个时间选择器微信小程序原生的 picker 组件只支持单时间点但车位预约需要时间段所以要么用两个 picker 分别选开始和结束时间要么用第三方组件库里的 range 选择器。3.2 登录态处理wx.login 拿到 code后端换 openid微信小程序的登录机制和传统 Web 登录完全不同。用户打开小程序先调 wx.login()拿一个临时 code然后把这个 code 发给后端后端用 code 去微信接口换 openid 和 session_key。这个 code 五分钟有效而且只能用一次。// pages/login/login.js const login () { wx.login({ provider: weixin, success: (res) { if (res.code) { // 把 code 发给后端后端去换 openid wx.request({ url: https://your-api.example.com/api/auth/login, method: POST, data: { code: res.code }, success: (resp) { const { token, userInfo } resp.data.data; wx.setStorageSync(token, token); wx.setStorageSync(userInfo, userInfo); } }); } } }); };后端收到 code 后用 appId 和 appSecret 调微信的 jscode2session 接口拿到 openid。openid 是用户在小程序里的唯一身份标识拿它当用户表的业务主键。这里有个细节session_key 不要存在你自己的数据库里更不要返回给前端。session_key 是用来解密敏感信息的存到服务端日志里都有泄露风险。token 的生成可以用 JWT把 userId 和过期时间塞进去过期时间建议 7 天够用且安全。3.3 车位状态展示与预约请求的发送车位列表渲染走常规的 wx:for 循环状态颜色通过三元表达式绑定 class。这里要注意一个性能细节如果车位数量超过 50 个不要一次性 setData 全部数据小程序 setData 是整页刷新的数据量太大会白屏。分段加载或者用 WXS 处理一下都行。预约请求的核心是检查“我要的时间段内这个车位是否可约”。这个判断放在后端做前端只管组装参数。前端要传的参数有四个车位 ID、开始时间、结束时间、用户 token。开始时间要校验不能早于当前时间结束时间要晚于开始时间时段跨度建议限制在 4 小时内防止有人一次约一整天导致车位被长期占用。// pages/booking/booking.js const submitBooking () { const { spaceId, startTime, endTime } this.data; if (!startTime || !endTime) { wx.showToast({ title: 请选择时间段, icon: none }); return; } if (new Date(startTime).getTime() Date.now()) { wx.showToast({ title: 不能预约过去的时间, icon: none }); return; } wx.request({ url: https://your-api.example.com/api/reservation/create, method: POST, data: { spaceId, startTime, endTime }, header: { Authorization: Bearer ${wx.getStorageSync(token)} }, success: (res) { if (res.data.code 0) { wx.showToast({ title: 预约成功, icon: success }); // 跳转到订单页 wx.navigateTo({ url: /pages/orders/orders }); } else { wx.showToast({ title: res.data.msg, icon: none }); } } }); };这段代码里有三个细节值得说。第一时间校验不能只信前端后端必须再做一次因为小程序可以被抓包绕过。第二header 里带 Authorization 是标准做法token 不要放在请求体里。第三预约成功之后跳订单页而不是弹窗完事让用户立刻看到自己的订单记录体验更完整。4. 后端核心逻辑并发预约、状态机流转与定时任务4.1 预约接口的并发控制乐观锁 唯一索引双保险这是整个系统的灵魂。两个用户同时抢同一个车位时间都选的是 18:00-19:00两个请求都查了车位状态发现空闲然后都插入了订单——这不就超卖了吗常见做法是三种方案选其一或者像我一样叠加使用。方案一悲观锁。用 SELECT ... FOR UPDATE 锁住车位行事务里再检查订单冲突。方案二乐观锁。UPDATE parking_space SET status1, versionversion1 WHERE id? AND version?更新影响行数为 0 说明被人抢了。方案三数据库唯一索引。给 order 表加一个 (space_id, start_time, end_time) 的唯一索引重复插入直接报错。我一般用乐观锁 唯一索引双保险。乐观锁负责控制车位的并发占用唯一索引兜底防止极端情况下订单重复。先写检查代码再插入订单最后更新车位状态三步在一个事务里完成要么全成功要么全回滚。这里的关键是先插订单再改车位状态。顺序反了会出现订单没建成但车位被锁死的状态。// ReservationService.java Transactional(rollbackFor Exception.class) public ReservationResult createReservation(CreateReservationRequest request) { // 1. 校验参数时间合法、车位存在、用户合法 ParkingSpace space parkingSpaceMapper.selectById(request.getSpaceId()); if (space null || space.getStatus() 2) { return ReservationResult.fail(车位不存在或维护中); } // 2. 乐观锁更新车位状态从空闲改为占用 int updated parkingSpaceMapper.updateStatusByVersion( request.getSpaceId(), space.getVersion(), 1 ); if (updated 0) { return ReservationResult.fail(车位已被抢占请刷新重试); } // 3. 查询该车位在时间段内是否有冲突订单 int conflictCount reservationOrderMapper.countConflict( request.getSpaceId(), request.getStartTime(), request.getEndTime() ); if (conflictCount 0) { // 回滚车位状态 parkingSpaceMapper.updateStatus(request.getSpaceId(), 0); return ReservationResult.fail(该时间段已被预约); } // 4. 插入订单 ReservationOrder order new ReservationOrder(); order.setOrderNo(generateOrderNo()); order.setUserId(request.getUserId()); order.setSpaceId(request.getSpaceId()); order.setStartTime(request.getStartTime()); order.setEndTime(request.getEndTime()); order.setStatus(0); // 待生效 reservationOrderMapper.insert(order); return ReservationResult.success(order); }逻辑说明第 2 步用乐观锁把车位从 0 改成 1这一步保证了同一时刻只有一个人能改成功。第 3 步再查时间冲突第 4 步插入订单。注意第 3 步如果发现冲突要把车位状态回滚成 0否则车位就永久锁死了。乐观锁的作用是控制“车位状态修改”的并发冲突检查的作用是防止“时间段重叠”两者职责不同不要混为一谈。4.2 订单状态机待生效、生效中、已完成、已取消、超时未到状态机设计直接决定你后面写业务逻辑的复杂度。我把订单状态定成五个0-待生效、1-生效中、2-已完成、3-已取消、4-超时未到。状态转移的规则是待生效 → 用户取消变成已取消待生效 → 到了开始时间变成生效中生效中 → 用户确认离场变成已完成生效中 → 超过结束时间还没确认自动变成已完成待生效 → 过了开始时间还没到场变成超时未到。这个状态机里最有价值的是“超时未到”这个状态。很多第一版系统只有“预约中/已完成/已取消”三个状态结果用户预约了 18:00 的车位18:30 才到车位在 18:00-18:30 这段时间被一个没到场的人占着名字真正需要车位的人约不了。超时未到状态配合一个“最早可超时时间”参数比如开始时间后 15 分钟就能把僵尸订单清掉释放车位给其他人。// 定时任务每5分钟扫描一次处理超时订单 Scheduled(cron 0 */5 * * * ?) public void handleTimeoutOrders() { // 1. 查询所有待生效且开始时间已过15分钟的订单 LocalDateTime deadline LocalDateTime.now().minusMinutes(15); ListReservationOrder timeoutOrders reservationOrderMapper.selectTimeoutOrders(deadline); for (ReservationOrder order : timeoutOrders) { // 2. 订单状态改为超时未到 reservationOrderMapper.updateStatus(order.getId(), 4); // 3. 释放车位 parkingSpaceMapper.updateStatus(order.getSpaceId(), 0); } }定时任务这里有个坑Spring 的 Scheduled 默认是单线程的如果你的系统里有多个定时任务要配置线程池。另外任务里处理订单要和数据库事务配合好每处理一个订单单独开一个事务不要全部包在一个大事务里否则第 100 个订单失败了前 99 个全回滚。4.3 接口设计规范统一返回体、异常处理、参数校验后端接口设计不复杂但必须统一。我习惯的返回格式是 { code: 0, msg: success, data: {} }code 为 0 表示成功非 0 表示业务错误。全局异常处理器捕获所有未预料的异常返回 code 500。参数校验不要散落在业务代码里用 Spring Validation 注解写在 DTO 上代码清爽还省得漏校验。// CommonResult.java Data public class CommonResultT { private Integer code; private String msg; private T data; public static T CommonResultT success(T data) { CommonResultT result new CommonResult(); result.setCode(0); result.setMsg(success); result.setData(data); return result; } public static T CommonResultT fail(String msg) { CommonResultT result new CommonResult(); result.setCode(-1); result.setMsg(msg); return result; } }这里要提一个容易被忽略的点接口的幂等性。用户在小程序里手抖连点了两次“预约”按钮前端是可以通过 loading 状态锁住的但后端还是可能收到重复请求。最可靠的做法是在前端生成一个 requestId同一个请求带上后端用 Redis 的 setnx 保证相同 requestId 的请求只处理一次。课程设计如果不引入 Redis也可以用数据库的唯一索引配合一个 request_id 字段做幂等。4.4 管理员接口车位管理、订单查看、异常处理管理员端不需要复杂权限系统一个简单的 isAdmin 标识就够了。但接口还是要单独开一组不要和用户接口混在一起。核心接口有四个查询所有车位带分页、新增/编辑/删除车位删除前要检查有没有关联订单、查询订单列表按状态过滤、按时间排序、强制结束订单管理员手动关闭异常订单并释放车位。强制结束订单这个接口要格外小心。比如管理员点了一个“待生效”订单的强制取消那就直接改状态为已取消、释放车位如果点的是一个“生效中”订单的强制结束那就把状态改成已完成同时记录操作日志。不要用一个接口同时处理所有状态状态不同释放逻辑和数据影响都不一样分开写更省心。5. 避坑指南微信小程序车位预约的 5 个高频翻车点5.1 预约成功后车位仍显示空闲状态缓存与刷新机制不一致现象用户预约成功后返回订单页再回首页车位状态还是绿色空闲。原因首页的车位列表是之前请求的旧数据小程序没有自动刷新或者后端做了 Redis 缓存缓存过期时间太长。解决首页用 wx.show 的 onShow 生命周期里重新请求车位列表如果用了 Redis 缓存把车位状态缓存过期时间控制在 10 秒内或者直接不走缓存。这是出现频率最高的问题没有之一。5.2 并发预约时车位被锁死更新失败后没有回滚状态现象两个用户同时抢一个车位A 成功B 失败B 的失败回调显示“车位已被抢占”但刷新后车位变成了灰色占用状态A 的订单其实没创建成功。原因B 的请求在时间冲突检查时发现冲突抛了异常但车位状态在乐观锁那步已经改成 1 了事务回滚没把状态改回来。解决在代码里捕获时间冲突异常时显式执行车位状态回滚或者把车位状态更新放在订单插入之后用订单插入的成功与否决定车位是否占用。我更推荐后者先查冲突、再插订单、最后改车位状态三个步骤按这个顺序走冲突和失败都发生在车位状态改写之前。5.3 数据库时间差 8 小时预约的时间点和实际对不上现象用户在小程序里选了 18:00-19:00到了 18:30 还没生效查看数据库发现存的是 10:00-11:00。原因JDBC 连接串没有指定 serverTimezoneMySQL 的时区是 UTCJava 的 LocalDateTime 转换时按 UTC 解析了。解决所有环境的数据库连接串统一加 serverTimezoneAsia/Shanghai并且数据库初始化时执行 SET GLOBAL time_zone 8:00。另外前端传时间时统一传时间戳或者“YYYY-MM-DD HH:mm:ss”字符串不要传 ISO 8601 格式的带时区字符串否则后端解析还会有第二次偏差。5.4 定时任务不执行或执行多次单机部署误用分布式锁现象本机测试定时任务正常部署到服务器后有时不执行有时执行两遍订单状态混乱。原因部署了两台实例Spring 的 Scheduled 不会自动进行分布式协调每个实例都会执行一遍定时任务。解决课程设计阶段单机部署即可不用上分布式锁。如果一定要多机部署引入 Redis 分布式锁或者把定时任务抽成独立服务。最常见的血泪经验是答辩演示时开了两台后端定时任务跑了两次把正常订单顶掉了场面一度非常尴尬。5.5 用户取消预约但车位未释放状态更新不是原子操作现象用户点取消订单状态变成已取消车位仍然显示占用。原因取消订单和释放车位是两个独立的 SQL没有放在同一个事务里第一个成功、第二个失败就会出现不一致。解决取消操作要包在 Transactional 里先改订单状态为已取消再改车位状态为 0任何一个失败都要回滚。类似的场景还有“超时未到释放车位”“管理员强制结束订单”涉及双表更新的一律事务包裹。6. 进阶验证与扩展并发压测、缓存引入与“真正可用”的定义车位预约系统做完别急着写说明文档先做一轮验证。最值得做的验证是并发预约测试。用 Jmeter 或 Postman 的 Collection Runner模拟 20 个线程同时预约同一个车位同一个时间段看成功数量是不是 1失败的是不是 19失败信息是不是预期的提示。这个测试跑完你的乐观锁和唯一索引到底有没有生效一目了然。如果出现了成功数大于 1说明要么版本号没起作用要么唯一索引没建上回头查 SQL。第二个值得做的验证是状态流转测试。手动创建一个预约订单把开始时间改成过去 20 分钟等定时任务跑完看订单是否变成超时未到、车位是否释放。再把另一个订单改成生效中调用确认离场接口看订单是否变成已完成。这一套流程走完状态机的六个转移路径基本就验证全了。扩展方向我只说一个最有价值的给车位状态加 Redis 缓存。当车位数量超过 200 个MySQL 扛并发读开始吃力前端首屏加载车位列表要等半天。把车位列表缓存到 RedisKey 设计成 parking_space:listValue 直接存 JSON缓存时间 30 秒。预约成功后删除缓存强制刷新。这套缓存逻辑写完系统就从“能答辩”跨到“能小规模上真实场景”。不过别过度设计课程设计阶段引入 Redis 是加分项不是必需品。最后说一条我从项目里摔出来的教训不要为了展示工作量堆功能比如加一个没用的聊天模块或者积分商城。车位预约系统最有价值的是它的并发控制和时间状态流转把这两个点做扎实、能讲清楚设计思路答辩老师问什么你都能接住。这套系统做完你会对“小程序端 后端接口 数据库表设计 状态管理”有完整的体感。这个经验迁移到任何预约类系统——会议室预约、实验室预约、健身房时段预约——都是相通的。希望这篇笔记帮你在实现它的路上少走几步弯路。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门