用JAVA做了一套停车系统,找位、计费、余位实时联动
做Java后端这几年,我一直在想一个问题:什么样的项目才算“有含金量”?互联网大厂那套高并发秒杀、分布式事务,确实高大上,但小团队、个人开发者很难有真实业务场景去练手。直到有个周末,我在商场地库绕了三圈才等到一个车位,回家后越想越气,干脆自己做了一套用JAVA写的停车系统。做完我才发现,停车这个场景太适合做Java练手项目了——它有真实的并发问题、有清晰的业务流程、有订单状态机、有计费规则,几乎把后端开发的核心知识点全串了一遍。
这篇文章会把整套系统的设计思路、核心代码、踩坑记录全部摊开来讲。不是给你贴一堆没法落地的架构图,而是告诉你:车位余量在并发下怎么做到不超卖,停车订单的计费状态怎么设计才不容易出bug,车牌识别的异步回调怎么跟主流程解耦。如果你正准备找一个Java实战项目,或者工作中刚好要接停车、园区、场馆类的预约计费系统,这篇应该能帮你省下不少试错时间。
1. 从“找位难”到“Java做停车系统”:为什么这个项目值得动手做
先说一个很多人对停车系统的误解:以为它就是个CRUD,无非是车位表、订单表,增删改查而已。真动手做一遍,你会发现这个判断错得离谱。停车系统表面上是对“车”和“位”的管理,但本质上是对“不断变化的占用状态”在时间轴上的管理。
1.1 汽车进场的瞬间发生了什么
一辆车从进场开始,后台就需要完成一连串动作:识别车牌、检查是不是月卡用户、找最近的车位、把车位标记为占用、生成一条入场记录、通知大屏和公众号更新余位。其中任何一个环节慢了,道闸就会堵车。而如果入场后没有选车位这个动作,车辆停到哪个车位是无法精确感知的——除非引入了地磁感应或视频桩。所以第一版系统我做了个取舍:入场时分配车位,出场时释放车位,这样余位数量在逻辑上是严格守恒的,不会出现“显示有5个空位,但进了一辆车后还是5个”这种扯淡情况。
这里就涉及第一个核心问题:余位扣减的原子性。最早我的实现是查询余位,减一,再写回。结果很简单,并发进来两辆车,都读到余位是10,都减一,写回都是9——本该剩8,结果剩9。这个只有真正压测才能暴露的问题,我在第5部分会详细讲。
1.2 为什么说停车系统的复杂度刚好卡在“中级偏上”
我在给这篇文章定调的时候想了很久,停车系统的复杂度其实卡在一个很舒服的位置:比纯CRUD系统难,又没有订单交易系统那么难。它涉及几个典型的后端难点,但每个都控制在“跳一跳够得着”的程度:
- 并发控制:早晚高峰进场出场并发高,余位、订单状态都可能出现竞争。
- 状态流转:停车订单从入场、计费中、已缴费、离场,状态是有方向性的,乱跳就会让计费出问题。
- 时间敏感计算:不同时段单价不同,跨天停车怎么拆段计费,这是纯粹的业务逻辑难题。
- 异步解耦:车牌识别、支付回调、消息推送天然适合异步处理。
这四个点,任何一个拿出来都够写篇技术博客,而一套停车系统把它们全部有机地串了起来。所以无论你是准备面试、做毕业设计,还是想在公司内部建一套园区停车管理工具,这个项目都能成为很好的载体。而且它足够贴近生活,需求不依赖特定行业知识,产品逻辑一听就懂。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 业务闭环梳理:角色、状态机与核心流程再定义
做系统第一件事不是写代码,而是把业务流程捋清楚。停车系统绕不开三种角色:车主、停车场运营方、管理员。其中车主关心的是哪里有空位、收费多少、怎么缴费;运营方关心的是营收统计、车位利用率、异常订单;管理员关心的是车位配置、费率设置、设备绑定。
2.1 停车订单的完整生命周期
我设计的停车订单状态机如下:
| 状态 | 含义 | 可流转到的状态 | 触发条件 |
|---|---|---|---|
| 0 | 已入场(计费中) | 1, 2 | 出场识别/人工完结 |
| 1 | 已缴费待出场 | 2 | 道闸验证出场 |
| 2 | 已完成 | 无 | 车辆离场,订单归档 |
| 3 | 异常锁定 | 0, 2 | 无入场记录出场/设备故障 |
把“已缴费”和“已离场”拆成两个状态,是很多初写者容易漏掉的。实际场景中车主常常是提前扫码缴费,然后在15分钟内离场。如果缴费动作直接让订单变成“已完成”,缴费和离场之间那段空档就失控了——你不知道这辆车到底走没走,车位到底是空的还是被一辆“已缴费”的车占着。
2.2 核心流程的时序逻辑
一套停车流程大概可以简化成下面这个时序逻辑:
- 车辆驶入,地感线圈触发,摄像头抓拍。
- 识别服务返回车牌号,入场接口收到通知。
- 检查黑名单(逾期未缴费的车辆)。
- 查询可用车位,分配一个,车位状态改为占用。
- 生成入场记录及待计费订单。
- 车辆出场,再次识别车牌,读取订单。
- 如果是月卡且有效期,直接抬杆;临时车需要计算费用。
- 支付回调成功后,订单状态改成已缴费待出场,道闸抬杆。
- 车辆完全离场,地感触发,车位释放,订单完成。
这套流程每一步都有失败场景:识别失败怎么办?支付回调丢了怎么办?出场时发现没有入场记录怎么办?我在开发时给每个异常分支都准备了兜底方案,第7部分会专门讲这些坑。
3. 技术选型与工程骨架:Spring Boot + MySQL + Redis的组合取舍
技术选型不必花哨,但每个选择都必须有理由。我是基于“中小型停车场(200-500个车位)、单停车场起步、后续可扩展多停车场”的假设来设计的。在这个前提下,技术栈选用如下:
- Spring Boot 2.7.x:Java生态最主流的快速开发框架,适合快速迭代,社区资料多,遇到问题好排查。
- MySQL 8.0:核心业务数据存储,车位、订单、费率等强一致数据都在这里。为什么不用NoSQL?订单有复杂的状态流转和统计维度,关系型数据库的约束和事务能省掉很多脏数据问题。
- Redis 6.x:承担余位缓存、分布式锁、短时热点数据。选择Redis而不是本地缓存,是因为后续如果扩展到多实例部署,本地缓存会各自为政,余位数据就乱了。
- MyBatis-Plus:单表CRUD不用手写SQL,复杂的联表统计再自定义SQL。
- Redisson:基于Redis的分布式锁和信号量,解决并发扣减问题。
- RabbitMQ:处理车牌识别回调、支付结果回调、离场通知等异步事件。
- Vue 3 + Element Plus:管理后台,配合ECharts做可视化。
3.1 为什么不用微服务架构
一句话回答:没必要。一个几百车位的小型停车场,单机Spring Boot就能扛住远高于实际的并发量。硬拆成微服务,光服务间调用、配置中心、链路追踪就够喝一壶的,对项目本身毫无帮助。但我在代码结构上预留了模块化边界:parking-api、parking-biz、parking-dao、parking-common,后续真要拆分,按模块切出去即可。这个思路我觉得比一上来就上Spring Cloud靠谱得多。
3.2 工程结构说明
bash复制parking-system
├── parking-api # 对外接口层(Controller层)
├── parking-biz # 核心业务服务层(Service)
├── parking-dao # 数据访问层(Mapper)
├── parking-common # 通用工具、常量、异常定义
├── parking-job # 定时任务(超时订单处理、对账)
└── parking-admin # 管理端接口与页面资源
依赖关系自上而下:api依赖biz,biz依赖dao,common被所有人依赖。注意job和admin是平级的业务入口,而不是api的一个子模块,这样后面接小程序端、公众号端都很清晰。
4. 数据库设计:车位、订单、计费规则三张表怎么联动
数据库设计是整套系统的地基。停车场的核心表其实不多,但坑不少。我把最核心的几张表结构和设计思路摆出来,直接照着做基本不会出大问题。
4.1 车位表(parking_space)
sql复制CREATE TABLE `parking_space` (
`id` bigint NOT NULL
