尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

SpringBoot+Vue智慧停车管理系统设计与实现:从数据库设计到微信支付

发布时间:2026/9/29 17:02:33

资讯中心
01
ARTICLE

SpringBoot+Vue智慧停车管理系统设计与实现:从数据库设计到微信支付

SpringBoot+Vue智慧停车管理系统设计与实现:从数据库设计到微信支付
刚做完一个基于SpringBootVue的智慧停车管理系统从需求分析、数据库设计到前后端联调、打包部署完整走了一遍。这篇就把整个项目的设计与实现过程拆开来讲包括表结构怎么设计、车位状态怎么同步、微信支付怎么接入、预约逻辑怎么处理以及一些实际踩过的坑给正在做类似系统或者准备拿这个方向做毕设的同学一个参考。先说一下这套系统最终实现了什么车主端小程序/H5可以实时查看停车场剩余车位、在线预约车位、停车缴费、查看停车记录管理端Web可以管理车位、设置收费标准、查看营收统计、处理异常订单。技术栈就是标题里写的SpringBootVue后端用SpringBoot 2.7 MyBatis-Plus MySQL Redis前端管理端用Vue 3 Element Plus车主端用的是Vue 3 Vant。整个系统前后端分离RESTful API通信JWT做身份认证。1. 项目整体设计与技术选型1.1 为什么选SpringBootVue这套组合这个组合在智慧停车这类管理系统里几乎是标配原因很实际。后端用SpringBoot核心价值在于它把Spring家族里那些繁琐的XML配置全部干掉starter机制让你引入一个依赖就自动配好一大片东西。比如接入Redis加一个spring-boot-starter-data-redis就完事连接池、序列化器都给你配好了。这对中小型管理系统来说开发效率提升非常明显不用花大量时间在环境搭建上。前端选Vue的原因也很直接。管理端页面大多是表格、表单、弹窗、图表这类交互Vue的双向数据绑定和组件化开发特别适合这种场景。Element Plus组件库把表格、分页、表单校验、对话框这些都封装好了写一个车位管理页面核心代码可能只需要几十行。车主端小程序或H5那边Vant组件库提供了移动端风格的操作界面上手也很快。还有一个关键点是前后端分离架构。在智慧停车这个场景里意味着停车场的道闸设备、地感线圈、摄像头这些硬件通过HTTP接口跟后端通信跟车主端、管理端完全解耦。设备上报一个事件后端更新车位状态前端轮询或通过WebSocket推送拿到最新数据整个链路非常清晰。1.2 系统整体架构整个系统我按四个层次来设计。表现层是车主H5/小程序和管理端Web网关层用Nginx做反向代理和静态资源托管业务层是SpringBoot微服务包括用户服务、车位服务、订单服务、支付服务、车辆识别服务数据层是MySQL存业务数据、Redis做缓存和分布式锁、MinIO存车牌识别照片和用户头像。车主端(H5/小程序) 管理端(Web) | | Nginx(反向代理静态资源) | | SpringBoot API Server / | | | \ 用户 车位 订单 支付 车辆 \ | | | / MySQL Redis MinIO这个架构的好处是每一层都能独立扩展。比如高峰期车主端请求量大可以只给API服务加实例通过Nginx负载均衡不影响其他部分。数据库层面Redis扛住了绝大部分读请求MySQL的压力主要在订单写入和账单统计上。1.3 核心功能模块划分功能模块我分成三个端来规划。车主端包含注册登录、车位查询、车位预约、扫码支付、停车记录、电子发票。管理端包含车位管理、收费规则配置、车辆管理、订单管理、营收统计、管理员权限。硬件对接端包含车牌识别接口、道闸控制接口、余位屏数据推送。这里要说一下设计上的取舍。最开始也考虑过做月卡/年卡充值功能后来觉得在系统第一版里优先级不高而且涉及复杂的套餐逻辑和到期提醒就砍掉了。做项目一定要学会做减法先把核心业务闭环跑通再考虑增值功能。智慧停车最核心的闭环就是找车位-预约-入场-计费-出场-支付。2. 数据库设计与核心表结构2.1 数据表关系梳理这是整个项目的地基我花了两天时间来设计表结构和梳理关系。核心表一共有8张用户表、车位表、预约表、订单表、收费标准表、车辆表、停车记录表、管理员表。表之间关系不复杂但有几个细节需要特别注意。车位表和收费标准表是多对一关系一个停车场可以有多条收费标准比如首小时5元、之后每小时2元、24小时封顶20元这些是同一辆车在不同时长下触发的不同规则。预约表和车位表是多对一关系一个车位在某个时间段只能被一个预约占用这里涉及并发控制。订单表和预约表是一对一关系预约成功后必然会产生一条待支付订单。2.2 关键表结构详解用户表(t_user)。字段包括id、openid、phone、nickname、avatar、balance、status。openid是微信小程序端的唯一标识如果用H5端可以用手机号验证码登录然后生成token。balance字段是预留的后续如果做余额支付可以复用当前版本主要还是微信支付。车位表(t_parking_space)。字段包括id、parking_id、space_no、type、status、curr_vehicle_id。type区分普通车位和充电车位status有四种空闲(0)、占用(1)、预约(2)、维护(3)。这里有个设计细节为什么预约状态要单独一个值而不是直接标记为占用因为预约状态下的车位在预约时间段到达后会自动转为占用但在到达之前道闸是不能放行的。如果不区分这两种状态逻辑会混乱。预约表(t_reservation)。字段包括id、user_id、space_id、reserve_start、reserve_end、status、order_id。status有待入场(0)、已入场(1)、已取消(2)、已过期(3)。这里最关键的是预约时间段校验后面在并发控制部分细说。订单表(t_order)。字段包括id、order_no、user_id、vehicle_id、space_id、enter_time、leave_time、duration、amount、status、pay_time、pay_type。status有待支付(0)、已支付(1)、已完结(2)、已退款(3)、已关闭(4)。order_no用时间戳随机数生成同时加了唯一索引防止重复下单。收费标准表(t_fee_rule)。字段包括id、parking_id、rule_name、free_minutes、base_amount、base_minutes、per_hour_amount、cap_amount。还加了一个effective_time和expire_time来做规则有效期比如节假日可以启用临时费率。这块设计灵活一些后面改需求的时候会省很多事。2.3 索引设计与查询优化索引这块我踩过坑刚开始没加索引车位列表页在数据量到几万条的时候明显变慢。后来给订单表的user_id、enter_time、status分别加了索引查询速度从800ms降到了20ms左右。这个差距在真实场景下感受非常明显。核心索引如下ALTER TABLE t_order ADD INDEX idx_user_id (user_id); ALTER TABLE t_order ADD INDEX idx_enter_time (enter_time); ALTER TABLE t_order ADD INDEX idx_status (status); ALTER TABLE t_reservation ADD INDEX idx_user_id (user_id); ALTER TABLE t_reservation ADD INDEX idx_space_id (space_id);还有个细节是分页查询一定要用索引覆盖。比如查询某个用户的历史订单如果用WHERE user_id ? ORDER BY enter_time DESC LIMIT ?一定要建(user_id, enter_time)的联合索引这样排序也能走索引否则到后面数据量大时会触发filesort性能下降很严重。3. 核心功能实现与关键技术点3.1 用户认证与JWT鉴权登录流程用的是微信小程序登录jwt token的方式。前端调用wx.login获取code传给后端后端用code换openid查用户表如果不存在就自动注册存在就生成token返回。PostMapping(/login) public Result login(RequestBody LoginDTO dto) { String openid wxService.code2Session(dto.getCode()); User user userService.getByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); user.setNickname(微信用户 RandomUtil.randomNumbers(6)); userService.save(user); } String token jwtUtil.generateToken(user.getId(), user.getRole()); return Result.success(token); }JWT这块要特别注意token过期时间。我设置了access token有效期2小时refresh token有效期7天。车主端如果只在停车时打开一下2小时完全够用但如果要查历史记录可能就会过期。我用拦截器统一处理token刷新如果access token过期但refresh token有效就返回新token前端拿到新token后替换本地存储。拦截器这里还有个细节要处理就是一个请求里携带的token如果即将过期比如还剩5分钟我在响应头里加一个X-Refresh-Token标记前端检测到就静默刷新。这样用户体验最好不会出现用着用着突然要重新登录的情况。3.2 车位状态实时同步这是智慧停车系统的核心也是最容易出问题的环节。我设计了三级同步机制保证车位状态在车主端、管理端、道闸设备之间保持一致。第一级是数据库状态作为最终准确来源。所有状态变更都必须落库比如车辆入场时把车位状态从空闲改为占用出场时改为空闲。第二级是Redis缓存用于高并发读。车位数查询、车位列表这些高频读操作全部走缓存缓存key设计为parking:space:{id}:statusvalue是状态码。写入时用先写数据库再删缓存的模式避免缓存和数据库不一致。第三级是WebSocket推送用于车主端实时感知。当车位状态变化时后端通过WebSocket广播给在线用户。这里用Spring的SimpMessagingTemplate实现前端订阅/topic/parkingStatus主题。public void changeStatus(Long spaceId, Integer status) { spaceService.updateStatus(spaceId, status); redisTemplate.opsForValue().set(parking:space: spaceId :status, String.valueOf(status)); messagingTemplate.convertAndSend(/topic/parkingStatus, new StatusMessage(spaceId, status)); }三级同步顺序为什么是先数据库再Redis再WebSocket因为数据库是持久化兜底Redis是缓存加速WebSocket是通知推送。只要保证最终一致性即可不需要强事务。3.3 预约并发控制预约这块是并发问题最严重的。假设有100个人同时抢一个车位如果不用并发控制就会卖超。我用的是Redis分布式锁加数据库唯一约束的双保险方案。预约流程是这样Transactional public Result reserve(ReserveDTO dto) { // 1. 获取分布式锁 String lockKey lock:space: dto.getSpaceId(); boolean locked redisLock.tryLock(lockKey, 10, TimeUnit.SECONDS); if (!locked) { return Result.error(操作太频繁请稍后重试); } try { // 2. 校验车位状态 ParkingSpace space spaceService.getById(dto.getSpaceId()); if (space.getStatus() ! SpaceStatus.FREE) { return Result.error(车位已被占用或预约); } // 3. 校验时间段是否有冲突 int count reservationService.checkConflict(dto.getSpaceId(), dto.getStartTime(), dto.getEndTime()); if (count 0) { return Result.error(该时段已被预约); } // 4. 创建预约和订单 Reservation reservation new Reservation(); reservation.setUserId(...); reservation.setSpaceId(...); reservation.setStatus(ReservationStatus.PENDING); reservationService.save(reservation); Order order new Order(); order.setOrderNo(generateOrderNo()); order.setAmount(calcAmount(dto.getStartTime(), dto.getEndTime())); orderService.save(order); // 5. 更新车位状态为预约中 space.setStatus(SpaceStatus.RESERVED); spaceService.updateById(space); return Result.success(order.getId()); } finally { redisLock.unlock(lockKey); } }这里有几个关键点。一是tryLock加超时时间防止某个请求占用锁后异常退出导致死锁。二是事务和锁的顺序先加锁再开事务这样锁的粒度能包住整个业务逻辑。三是预约时间冲突校验SQL要写start_time ? AND end_time ?取反就是不冲突的判断条件。3.4 停车计费逻辑计费是业务规则最复杂的部分我踩了好几次坑。最初以为就是简单的“首小时5元之后每小时2元”实际写代码才发现边缘情况特别多入场不到15分钟免费、跨天计费、24小时封顶、节假日费率、充电车位额外收费。计费核心逻辑public BigDecimal calcAmount(LocalDateTime enterTime, LocalDateTime leaveTime, FeeRule rule) { long minutes Duration.between(enterTime, leaveTime).toMinutes(); // 免费时长内 if (minutes rule.getFreeMinutes()) { return BigDecimal.ZERO; } BigDecimal amount BigDecimal.ZERO; // 首小时费用 if (minutes rule.getBaseMinutes()) { return rule.getBaseAmount(); } amount amount.add(rule.getBaseAmount()); // 超出首小时的费用 long extraMinutes minutes - rule.getBaseMinutes(); amount amount.add( BigDecimal.valueOf(extraMinutes) .divide(BigDecimal.valueOf(60), 2, RoundingMode.CEILING) .multiply(rule.getPerHourAmount()) ); // 封顶逻辑 if (rule.getCapAmount() ! null amount.compareTo(rule.getCapAmount()) 0) { return rule.getCapAmount(); } return amount; }最坑的是超时部分向上取整的问题。2小时01分和3小时整如果按分钟精确算单价会不一样。实际停车场的规则是超时按小时继续计费不足一小时按一小时算所以用了RoundingMode.CEILING。还有场景是入场时间和出场时间跨天。比如晚上10点进场凌晨2点出场数据库存的是时间戳没问题但计费时Duration.between计算出来的分钟数包含跨天这个逻辑本身没问题。真正的问题在报表统计按天聚合营收时要按照场时间来判断归属哪一天我当时在这里纠结了很久。3.5 微信支付对接微信支付这块是很多人的痛点我讲一下整体流程。用户点击支付后端调用微信支付统一下单接口拿到prepay_id返回给前端前端调起微信支付收银台用户输入密码支付微信后台异步通知后端支付结果。public String createPayOrder(Long orderId) { Order order orderService.getById(orderId); // 构造微信支付参数 MapString, String params new HashMap(); params.put(appid, wxConfig.getAppId()); params.put(mch_id, wxConfig.getMchId()); params.put(out_trade_no, order.getOrderNo()); params.put(total_fee, order.getAmount().multiply(BigDecimal.valueOf(100)).intValue() ); params.put(body, 智慧停车-停车费); params.put(notify_url, wxConfig.getNotifyUrl()); params.put(trade_type, JSAPI); params.put(openid, wxConfig.getOpenid(order.getUserId())); String prepayId wxPayService.unifiedOrder(params); return buildPayParams(prepayId); }一定要注意金额单位。微信支付以分为单位数据库存的金额以元为单位转换时用multiply(100).intValue()千万不能直接强转否则会有精度问题。我就踩过这个坑测试时发现支付金额多了1分钱。支付回调要处理幂等性。微信会多次通知同一个订单比如网络超时重发处理时先查订单状态如果已支付就直接返回成功不再重复处理。PostMapping(/wx/notify) public String wxNotify(RequestBody String xmlData) { MapString, String result wxPayService.parseNotify(xmlData); String orderNo result.get(out_trade_no); String tradeStatus result.get(result_code); Order order orderService.getByOrderNo(orderNo); if (order null || order.getStatus() OrderStatus.PAID) { return xmlreturn_codeSUCCESS/return_codereturn_msgOK/return_msg/xml; } if (SUCCESS.equals(tradeStatus)) { orderService.paySuccess(orderNo, result.get(transaction_id)); } return xmlreturn_codeSUCCESS/return_codereturn_msgOK/return_msg/xml; }3.6 车牌识别与道闸联动车牌识别我用的是硬件厂商提供的SDK停车场入口的摄像头检测到车牌后把车牌号和抓拍照片推送到后端接口后端判断车辆是否有预约或是否为月卡用户决定是否开闸放行。这里涉及一个异步处理的细节。摄像头推送车牌识别结果后如果同步去查数据库再控制道闸整个过程可能需要2-3秒车辆在闸口的等待体验很差。我用消息队列异步处理摄像头推送后立即返回响应后台把车牌识别结果丢进MQ消费者处理放行逻辑。PostMapping(/device/recognize) public Result recognize(RequestBody RecognizeDTO dto) { // 立即返回异步处理 mqTemplate.send(parking.recognize.queue, dto); return Result.success(已接收); } RabbitListener(queues parking.recognize.queue) public void handleRecognize(RecognizeDTO dto) { Vehicle vehicle vehicleService.getByPlateNo(dto.getPlateNo()); if (vehicle ! null) { // 有登记开闸放行 gateService.openGate(dto.getGateId()); // 更新车位状态 parkingService.vehicleEnter(vehicle.getId(), dto.getSpaceId()); } else { // 无登记车辆走临时车流程 parkingService.tempVehicleEnter(dto); } }3.7 前端Vue关键实现管理端用的Vue 3 Element Plus ECharts。布局是经典的后台管理布局左侧菜单栏右侧内容区。路由用vue-router鉴权用路由守卫请求用axios封装。登录路由守卫这里分享一个我处理过的坑。用户刷新页面后Vue实例重新加载store里的用户信息会丢失。我的处理是路由守卫里每次检查localStorage里有没有token如果有但store里没有用户信息就调用/user/info接口重新获取。router.beforeEach(async (to, from, next) { const token localStorage.getItem(token); if (token) { if (!store.state.userInfo) { try { const res await userApi.getInfo(); store.commit(setUserInfo, res.data); } catch (e) { localStorage.removeItem(token); next(/login); return; } } } if (to.meta.requiresAuth !token) { next(/login); } else { next(); } })车位管理页面是典型的CRUD界面用Element Plus的el-table绑数据el-dialog做新增编辑el-form做表单校验分页用el-pagination。模板代码看起来很长但核心逻辑很简单难点在状态切换时的即时刷新。比如管理员把一个车位改成维护状态已经订阅了WebSocket的页面会自动收到更新这里有个重复渲染的问题需要判断消息是否来自当前管理员操作。车主端用Vue 3 Vant核心页面是首页车位列表、预约页面、订单页面。首页车位列表用轮询加WebSocket双通道WebSocket断线时自动切换到轮询模式每10秒请求一次车位数。这个降级方案很实用WebSocket在弱网环境下非常不稳定必须有备用方案。4. 系统部署与性能优化4.1 前后端部署方案部署我用了Docker Compose把SpringBoot应用、Nginx、MySQL、Redis全部容器化。后端打的jar包做一个镜像前端构建出来的dist目录挂载到Nginx容器里Nginx里配置了/api前缀的反向代理。这套方案在单台2核4G的云服务器上跑得很稳。version: 3 services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 volumes: - ./data/mysql:/var/lib/mysql redis: image: redis:7.0 backend: build: ./backend depends_on: - mysql - redis environment: SPRING_PROFILES_ACTIVE: prod frontend: image: nginx:alpine volumes: - ./frontend/dist:/usr/share/nginx/html - ./frontend/nginx.conf:/etc/nginx/conf.d/default.conf ports: - 80:804.2 性能优化实践系统上线前做了压测用的是JMeter模拟200个并发用户同时查车位列表和提交预约。压测结果暴露了三个问题一是MySQL连接池默认只有10个并发一高就报连接不够二是车位列表查询每次都查数据库没有走缓存三是预约事务时间过长导致锁等待严重。优化方案分别是连接池调到50最大等待时间5000ms车位列表接口加Redis缓存缓存5秒预约流程去掉不必要的事务嵌套减少锁持有时间。优化后压测结果TPS从原来的200涨到800接口响应时间从600ms降到100ms左右。还有一个优化点是静态资源前端打包后的JS/CSS通过Nginx开启gzip压缩带宽占用下降60%首屏加载时间从3秒降到1秒以内。4.3 数据库读写分离与高可用如果数据量上来单库单表会撑不住。我的规划是先用读写分离解决读压力主库写订单、车位状态从库读历史订单、统计报表。MySQL主从复制配起来不难但应用层要区分数据源这里用ShardingSphere或MyBatis-Plus的多数据源插件都能实现。不过我要提醒一句做毕业设计或者演示项目完全没有必要上微服务、读写分离、消息队列这些重量级的东西单机单库足够了。把Redis缓存、索引优化、连接池调优这些做好10万级的数据量完全没问题。4.4 常见部署问题速查问题现象可能原因解决方法前端页面打开白屏后端API无法访问或静态资源路径错误检查Nginx代理配置和dist路径接口报401token过期或JWT密钥不匹配检查JWT配置确认前后端密钥一致二维码支付报错回调地址是内网地址微信无法访问配置公网IP或内网穿透地址登录后刷新页面跳回登录页store状态丢失路由守卫里增加用户信息重新获取逻辑微信群发通知无法发送小程序订阅消息模板ID配置错误检查模板ID和用户订阅状态5. 项目测试与常见问题排查实录5.1 功能测试清单梳理测试阶段我列了一个清单把系统所有功能点过了一遍。登录注册、车位列表、车位预约、订单支付、扫码出场、停车记录、后台管理、收费规则一共8大模块37个测试用例。重点测的是预约流程完整链路用户预约车位-收到预约成功通知-到场扫码入场-停车-计费-支付-出场-车位状态恢复。测出来几个问题除了前面说过的微信支付精度问题还有一个是预约过期状态没有定时任务去清理。预设了用户预约了车位但没到场到了预约结束时间车位还显示预约中别人看不了也约不了。后来加了定时任务每5分钟扫一次预约表把过期未入场的预约改成过期状态同时释放车位。5.2 并发场景测试实录我用JMeter模拟了50个用户同时预约同一个车位测试结果只有1个用户成功其他49个都返回“车位已被预约”。这个结果说明分布式锁和状态校验是生效的。但是也暴露了一个问题很多用户挤在提交预约接口响应时间从正常时的150ms涨到800ms因为锁等待排队了。后来做了优化在进入预约接口之前先用Redis的计数器做了一层限流比如每秒最多处理500个预约请求超过的返回“系统繁忙”。这个限流方案效果很好接口响应时间稳定在300ms以内。还有一个并发问题是出库时的重复开闸。摄像头识别到同一个车牌可能因为光线变化等原因触发两次识别事件导致两次调开闸接口。处理方式是在设备服务层做了幂等同一车牌5秒内的重复开闸请求直接忽略。5.3 日常运维排查技巧上线之后最常遇到的问题不是代码bug而是环境问题。我总结了几个排查技巧接口突然变慢先看Redis连接数连接数过高可能是有慢查询阻塞了业务再比如某个时间段车位状态不更新先看RabbitMQ消费者是否正常消费者线程挂掉会导致消息堆积。日志排查用关键字搜索比如搜“Exception”或“ERROR”再看对应的请求ID关联上下游日志。我在日志里统一加了traceId前端传的请求ID会贯穿整个链路排查问题非常方便。这套系统从设计到落地前前后后花了一个多月时间。回过头来看最有价值的收获不是代码量而是对整个业务闭环的理解车位状态怎么在各种操作下保持一致、计费规则怎么设计才能既灵活又可控、并发场景下怎么保证数据不错不乱。最后再分享一个通用技巧做这类管理系统先把所有涉及状态变更的操作列表理出来逐条核对状态流向就能避免大部分逻辑混乱的问题。项目文档和关键代码片段我整理在本地了有需要的可以留言交流。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。