基于 Spring Boot + Vue3 的停车场预约小程序设计与实现
温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系名片!
一、项目背景与需求分析
随着城市机动车保有量持续增长,停车难、找车位耗时、车位利用率低等问题日益突出。传统停车场多依赖人工收费与现场排队,车主无法提前获知车位余量,管理者也难以掌握实时车位状态。基于此,本文设计并实现一套停车场预约小程序,让车主可以提前查询车位、在线预约、扫码入场与离场结算,同时为停车场管理者提供车位与订单的可视化管理能力。
系统核心需求可归纳为以下几类:
- 用户端:注册登录、停车场与车位查询、在线预约、预约取消、订单支付、历史订单查看、车辆信息管理。
- 管理端:停车场信息维护、车位管理、预约订单审核与统计、收费规则配置、用户与车辆管理。
- 系统能力:预约时段冲突校验、超时未入场自动释放车位、并发预约下的车位一致性保障。
二、技术选型与整体架构
系统采用前后端分离架构,后端基于 Spring Boot 提供 RESTful 接口,前端分为管理后台与微信小程序两端,数据库使用 MySQL,缓存使用 Redis。
| 层次 | 技术选型 | 说明 |
|---|---|---|
| 后端框架 | Spring Boot 2.7 / 3.x | 快速构建 Web 服务,内置 Tomcat |
| 持久层 | MyBatis-Plus | 简化 CRUD,支持分页与条件构造器 |
| 数据库 | MySQL 8.0 | 存储用户、停车场、车位、订单等数据 |
| 缓存 | Redis | 缓存车位余量、分布式锁、预约防重 |
| 鉴权 | Spring Security + JWT | 无状态登录鉴权,区分用户与管理员角色 |
| 管理后台 | Vue3 + Vite + Element Plus | 组合式 API 开发,组件化页面 |
| 小程序端 | uni-app(Vue3 语法) | 一套代码编译到微信小程序 |
整体调用链路为:小程序或管理后台发起请求,经 Nginx 转发至 Spring Boot 服务,服务层完成业务校验后访问 MySQL 与 Redis,最终以统一响应体返回前端。
flowchart LR A[微信小程序] --> C[Nginx] B[Vue3 管理后台] --> C C --> D[Spring Boot 应用] D --> E[(MySQL)] D --> F[(Redis)] D --> G[微信支付/登录接口]三、数据库设计
围绕停车场预约的核心业务,设计以下主要数据表:
sys_user:用户表,存储手机号、昵称、角色、状态等。user_car:车辆表,存储车牌号、车辆类型、所属用户。parking_lot:停车场表,存储名称、地址、经纬度、总车位数、营业时间。parking_space:车位表,存储车位编号、所属停车场、车位类型、状态。reservation_order:预约订单表,存储预约人、车位、预约时段、订单状态、金额。payment_record:支付记录表,存储订单号、支付方式、支付时间、支付状态。
其中预约订单表是业务核心,关键字段设计如下:
CREATE TABLE reservation_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT '订单号', user_id BIGINT NOT NULL COMMENT '预约用户', lot_id BIGINT NOT NULL COMMENT '停车场ID', space_id BIGINT NOT NULL COMMENT '车位ID', plate_no VARCHAR(16) NOT NULL COMMENT '车牌号', start_time DATETIME NOT NULL COMMENT '预约开始时间', end_time DATETIME NOT NULL COMMENT '预约结束时间', amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '订单金额', status TINYINT DEFAULT 0 COMMENT '0待支付 1已预约 2已入场 3已完成 4已取消 5已超时', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), KEY idx_space_time (space_id, start_time, end_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;为提升同一车位在时间维度上的冲突查询效率,在space_id与预约时段上建立联合索引。
四、后端核心功能实现
4.1 统一响应与全局异常处理
后端统一使用Result包装响应体,并通过@RestControllerAdvice捕获业务异常与参数校验异常,避免异常堆栈直接暴露给前端。
@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMessage("success"); r.setData(data); return r; } public static <T> Result<T> fail(String message) { Result<T> r = new Result<>(); r.setCode(500); r.setMessage(message); return r; } }4.2 预约下单与并发控制
预约下单需要解决两个问题:一是同一车位在目标时段内不能被重复预约,二是高并发下不能出现超卖。实现思路为:先基于 Redis 分布式锁按车位维度加锁,再在数据库层做时段冲突校验,最后写入订单。
@Service @RequiredArgsConstructor public class ReservationService { private final ReservationOrderMapper orderMapper; private final ParkingSpaceMapper spaceMapper; private final RedissonClient redissonClient; public Result<String> reserve(ReservationDTO dto) { String lockKey = "lock:space:" + dto.getSpaceId(); RLock lock = redissonClient.getLock(lockKey); try { if (!lock.tryLock(3, 10, TimeUnit.SECONDS)) { return Result.fail("当前预约人数较多,请稍后重试"); } ParkingSpace space = spaceMapper.selectById(dto.getSpaceId()); if (space == null || space.getStatus() != 1) { return Result.fail("车位不可用"); } int conflict = orderMapper.countConflict( dto.getSpaceId(), dto.getStartTime(), dto.getEndTime()); if (conflict > 0) { return Result.fail("该时段车位已被预约"); } ReservationOrder order = buildOrder(dto); orderMapper.insert(order); return Result.success(order.getOrderNo()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return Result.fail("预约失败,请重试"); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } }对应的冲突统计 SQL 使用半开区间判断,避免相邻时段被误判为冲突:
SELECT COUNT(1) FROM reservation_order WHERE space_id = #{spaceId} AND status IN (1, 2) AND start_time < #{endTime} AND end_time > #{startTime};4.3 超时未入场自动释放
借助 Spring 定时任务,周期性扫描已预约但超过开始时间一定时长仍未入场的订单,将其置为超时状态并释放车位。
@Component @RequiredArgsConstructor public class OrderTimeoutTask { private final ReservationOrderMapper orderMapper; @Scheduled(cron = "0 */5 * * * ?") public void releaseTimeoutOrders() { LocalDateTime deadline = LocalDateTime.now().minusMinutes(15); orderMapper.updateTimeoutOrders(deadline); } }4.4 JWT 登录鉴权
用户登录成功后签发 JWT,前端在请求头携带Authorization,后端通过拦截器解析并写入安全上下文,同时区分普通用户与管理员权限。
public class JwtInterceptor implements HandlerInterceptor { private final JwtUtil jwtUtil; public JwtInterceptor(JwtUtil jwtUtil) { this.jwtUtil = jwtUtil; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { response.setStatus(401); return false; } Claims claims = jwtUtil.parse(token.substring(7)); UserContext.set(claims.getSubject(), claims.get("role", String.class)); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); } }五、Vue3 管理后台实现
管理后台使用 Vue3 组合式 API 与 Element Plus 构建,路由采用动态路由配合权限控制,接口请求统一封装 Axios。
import axios from 'axios' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE, timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( res => { const { code, message, data } = res.data if (code === 200) return data ElMessage.error(message || '请求失败') return Promise.reject(new Error(message)) }, err => { if (err.response?.status === 401) { localStorage.removeItem('token') location.href = '/login' } return Promise.reject(err) } ) export default request车位管理页面使用el-table展示车位列表,配合el-dialog完成新增与编辑,状态变更通过行内操作按钮触发,操作完成后局部刷新列表。
六、小程序端功能实现
小程序端基于 uni-app 开发,主要页面包括首页停车场列表、停车场详情与车位选择、预约确认、订单列表与个人中心。
- 首页:按距离与余位排序展示停车场,支持关键字搜索。
- 预约页:选择日期与时段,实时展示可选车位,提交后跳转支付。
- 订单页:按状态筛选订单,支持取消预约与查看入场码。
- 个人中心:管理车辆信息、查看历史订单与账户余额。
预约提交的核心逻辑如下:
const submitReservation = async () => { if (!form.spaceId || !form.startTime || !form.endTime) { uni.showToast({ title: '请完善预约信息', icon: 'none' }) return } const res = await request.post('/api/reservation', { spaceId: form.spaceId, plateNo: form.plateNo, startTime: form.startTime, endTime: form.endTime }) uni.showToast({ title: '预约成功', icon: 'success' }) uni.navigateTo({ url: `/pages/order/detail?orderNo=${res}` }) }七、系统测试与优化建议
测试阶段重点覆盖预约时段冲突、并发下单、超时释放与支付回调等场景。可使用 JMeter 对预约接口做并发压测,验证分布式锁与数据库唯一约束的配合效果。
后续优化方向包括:
- 引入消息队列异步处理支付回调与超时释放,降低主流程耗时。
- 车位余量使用 Redis 缓存并设置合理过期时间,减少数据库压力。
- 增加预约信用分机制,对频繁取消的用户做限制。
- 接入地图服务,实现停车场导航与室内车位引导。
八、总结
本文围绕停车场预约场景,完成了从需求分析、技术选型、数据库设计到后端预约核心逻辑、Vue3 管理后台与小程序端实现的全流程设计。通过 Redis 分布式锁与数据库时段冲突校验相结合,有效保障了并发预约下的车位一致性;通过定时任务实现超时订单自动释放,提升了车位周转效率。该方案结构清晰、易于扩展,可作为同类预约类系统的参考实现。