简介:毕业设计《停车场管理系统》完整项目包,面向计算机相关专业学生与开发者,覆盖车位管理、车辆进出、费用结算等核心业务,适用于校园、商业区等典型停车场景,是毕业设计或课程设计的实用参考。资源包共592个文件,压缩后142.93MB,主要包含89个Java源文件、184个class编译文件、135个XML配置与界面文件、66个JAR依赖库,另有HTML、JavaScript、JSP页面、SQL数据库脚本及Git仓库文件,几乎涵盖Web系统从后端逻辑、前端交互到数据库设计的完整技术栈,其中Java文件对应业务逻辑、XML负责配置与视图、SQL提供数据表结构。已有2528人学习,内容提供完整工程代码、数据库脚本与配置信息,可帮助读者梳理车牌识别、计费处理、RESTful接口、用户权限等模块的实现方式,也能在此工程基础上进行二次开发与功能扩展,是综合运用数据库、软件工程与AI技术的较好范例。
1. 毕业设计停车场管理系统:为什么年年有人选,年年有人卡在答辩前
停车场管理系统是毕业设计里出现频率最高的几类选题之一,因为它的业务边界清晰:车辆入场、出场、计费、会员管理、报表统计。看起来就是一套标准的增删改查,很多同学选它图的是稳妥。但真做到中期答辩前后才发现,最容易翻车的也正是这种“看起来简单”的题目——数据库表设计不合理导致计费金额对不上账,硬件设备异步回调导致出场时找不到入场记录,跨天停车把跨天计费算成一笔糊涂账。真正能让人信服的停车场管理系统,不是把页面做出来就完事,而是把车辆状态流转、计费规则、异常订单处理这三件事理顺。这篇笔记我按自己做过的一个模拟项目X的完整思路来讲,从技术选型、表结构、核心计费逻辑、前端页面到高频踩坑点,全程可以照着复现,适合正在做这个题目的同学,也适合想快速搭一套可演示原型的朋友。
2. 技术选型与系统架构:先把方案定死,再动手写代码
2.1 单体应用还是前后端分离
停车场管理系统如果只在本地演示,用单体架构反而比前后端分离更省事。常见做法是先用一个Spring Boot的Web应用承载所有接口和页面渲染,配合Thymeleaf做服务端页面。这么做的好处是部署简单、答辩演示时不容易因为前端跨域配置把页面调挂。如果后续想扩展成小程序或平板端,再把接口层单独拆出来也不迟。我见过不少同学一上来就搞Vue加Spring Boot分离架构,结果CORS配置、Token过期处理、打包静态资源就耗掉一周。对于毕业设计这个体量,数据库表不超过二十张、并发量按单台道闸设备处理,单体应用完全扛得住。
2.2 数据库选型与部署位置
数据库首选开源的MySQL,版本选5.7或8.0都行,重点是把utf8mb4字符集固定下来,别用默认的latin1,不然车辆的车牌号里有汉字或者生僻字时,存进去就是乱码。部署位置建议直接装在本地电脑上,不要为了显得高级去搞云数据库。答辩现场一旦网络波动,整个演示就变成“系统打不开”,这种场面很尴尬。如果导师要求必须有网络版效果,可以在自己电脑上装一个内网穿透工具来临时演示,但别把它当作正式部署方案,因为隧道服务不稳定。
2.3 硬件设备对接:识别一体机与道闸的两种连接方式
停车场管理系统最大的不确定性来自硬件层。常见做法是选一体机设备,也就是集成了车牌识别摄像头和道闸控制模块的机型,厂商一般会提供HTTP接口或TCP协议。HTTP接口最简单可靠,设备识别到车牌后往我们系统的入场接口发一条POST请求,系统返回是否抬杆;出场时设备再发一条出场请求,计费完成后返回放行结果。TCP方式通常需要维护长连接和心跳包,代码复杂一些,但响应更实时。如果学校实验室没有真实设备,可以下载某个免费版的停车场客户端模拟器来代替,让模拟器定时推送虚拟车牌数据,系统层面的逻辑完全一样。
2.4 本地开发环境的目录结构
一套标准的单体结构大概长这样。我把代码和SQL脚本分开目录存放,方便答辩前重新初始化数据库。
parking-system ├── src/main/java │ └── com/example/parking │ ├── controller # 接口控制器 │ ├── service # 业务逻辑 │ ├── mapper # MyBatis数据访问 │ ├── entity # 实体类 │ └── config # 配置类 ├── src/main/resources │ ├── mapper # SQL映射XML │ ├── static # 前端静态资源 │ └── templates # 页面模板 ├── sql │ └── init.sql # 建表语句和初始化数据 └── pom.xml这个结构不复杂,但足够支撑后面要讲的所有功能点。接下来进入最关键的数据库设计环节。
3. 数据库设计:停车场系统的表结构决定计费逻辑能否落地
3.1 核心表拆解:入场、出场、计费规则分开存
很多人喜欢把所有信息塞进一张大表,结果出场时状态字段改来改去,一条订单记录的入场时间和出场时间写在一起,跨天计费和优惠计算全都绕不开状态判断。我的做法是拆成三张核心表:入场记录表、出场记录表、计费规则表。入场记录表每来一辆车就插入一条,出场时才生成对应的订单记录,通过在入场记录表里标记已离场来防止重复计费。计费规则表单独存,让管理员可以不改代码就调整收费标准。
3.2 车辆入场记录表:字段设计与索引设置
我把入场记录表的核心字段列出来,这是停车系统的地基,字段设计必须一次到位。
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键自增 |
| plate_number | varchar(16) | 车牌号,带汉字 |
| entry_time | datetime | 入场时间 |
| entry_image | varchar(255) | 入场抓拍图片地址 |
| device_id | varchar(32) | 入口设备编号 |
| is_member | tinyint | 是否会员,0或1 |
| member_id | bigint | 会员ID,可空 |
| status | tinyint | 0在场,1已离场,2异常订单 |
| exit_order_id | bigint | 关联出场订单ID |
车牌号字段必须加索引,因为车辆入场后再次识别出场时,我们要用车牌号去查最近的入场记录。如果不加索引,数据量一旦上万,出场查询就会明显变慢。status字段建议也加一个普通索引,用于后台查询在场车辆列表。
3.3 计费规则表:按车型和时段设计,支持跨天
计费规则是整个系统最容易和导师辩论的部分。常见做法是设计成支持按小时计费、按次计费、免费时长、单日封顶四种组合。我采用的规则表结构大致是:
| 字段名 | 类型 | 说明 |
|---|---|---|
| rule_id | bigint | 规则ID |
| car_type | varchar(8) | 小型车、大型车、新能源 |
| free_minutes | int | 免费分钟数 |
| unit_price | decimal(10,2) | 每小时单价 |
| max_daily_charge | decimal(10,2) | 单日封顶金额 |
| is_weekend_diff | tinyint | 是否区分工作日和周末 |
| weekend_price | decimal(10,2) | 周末单价 |
计费逻辑不能只按“总停车时长乘以单价”来做,因为跨天停车时单日封顶会导致金额计算复杂化。比如某车停了30小时,前24小时按单日封顶算,后6小时另算,这必须通过规则表来支撑。为了让这套逻辑能跑通,建表时还需要补充初始化脚本,把常见车型和计费规则预置好。
3.4 建表SQL脚本:一条可以直接执行的版本
下面这个SQL是入场表和计费规则表的实际建表语句,已经去掉了冗余字段,方便直接复制到Navicat里执行。
CREATE TABLE entry_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, plate_number VARCHAR(16) NOT NULL, entry_time DATETIME NOT NULL, entry_image VARCHAR(255), device_id VARCHAR(32), is_member TINYINT DEFAULT 0, member_id BIGINT, status TINYINT DEFAULT 0 COMMENT '0在场 1已离场 2异常', exit_order_id BIGINT, INDEX idx_plate (plate_number), INDEX idx_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE charge_rule ( rule_id BIGINT PRIMARY KEY AUTO_INCREMENT, car_type VARCHAR(8) NOT NULL, free_minutes INT DEFAULT 15, unit_price DECIMAL(10,2) NOT NULL, max_daily_charge DECIMAL(10,2), is_weekend_diff TINYINT DEFAULT 0, weekend_price DECIMAL(10,2) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO charge_rule (car_type, free_minutes, unit_price, max_daily_charge) VALUES ('小型车', 15, 2.00, 20.00), ('大型车', 0, 5.00, 50.00), ('新能源', 30, 1.50, 15.00);索引和字段注释已经带上了,不需要额外说明。启动项目后如果报时区错误,在连接URL后面加上serverTimezone=Asia/Shanghai,这是本地环境最常见的启动问题。表建好后,下一步就是写订单生成的业务逻辑。
4. 核心计费逻辑实现:订单生成、跨天计费与状态流转
4.1 入场接口入场接口的幂等处理
设备识别车牌后会请求入场接口,如果同一个车牌在一分钟内被请求两次,系统不能生成两条入场记录。常见做法是入场前先查同一车牌有没有status为0的在场记录,如果有就直接返回到达现有的入场记录。这样既避免了车辆出场时因为重复入场导致的出场混乱,也为后面设备的重复回调留好了安全垫。
public EntryResult entry(String plateNumber, String deviceId, String imageUrl) { // 先查在场记录,防止重复入场 EntryRecord exist = entryRecordMapper.findByPlateAndStatus(plateNumber, 0); if (exist != null) { return EntryResult.alreadyInside(exist); } // 查会员信息,是会员则记录会员ID Member member = memberMapper.findByPlate(plateNumber); EntryRecord record = new EntryRecord(); record.setPlateNumber(plateNumber); record.setEntryTime(new Date()); record.setEntryImage(imageUrl); record.setDeviceId(deviceId); record.setStatus(0); if (member != null) { record.setIsMember(1); record.setMemberId(member.getId()); } entryRecordMapper.insert(record); return EntryResult.success(record); }这段代码的关键是exist != null的早返回逻辑。设备回调经常出现重发,如果没有这个判断,同一辆车会生成多条在场记录,出场时辆车会被重复计费。member查询放在入场阶段完成的好处是后续出场结算时不需要再关联会员表查询。
4.2 出场计费算法:免费时长、单日封顶与跨天
出场计费是系统里最需要细抠的算法。我写了一个computeCharge方法,传入入场记录和出场时间,按照计费规则逐步计算。
public BigDecimal computeCharge(EntryRecord entry, Date exitTime, ChargeRule rule) { long totalMinutes = (exitTime.getTime() - entry.getEntryTime().getTime()) / 60000; // 先扣除免费时长 long paidMinutes = totalMinutes - rule.getFreeMinutes(); if (paidMinutes <= 0) { return BigDecimal.ZERO; } // 如果规则没有封顶,直接按总时长计算 if (rule.getMaxDailyCharge() == null) { return rule.getUnitPrice() .multiply(BigDecimal.valueOf(Math.ceil(paidMinutes / 60.0))); } // 有单日封顶时,按自然天拆分计算 BigDecimal totalCharge = BigDecimal.ZERO; Date cursor = entry.getEntryTime(); while (cursor.before(exitTime)) { Date dayEnd = endOfDay(cursor); Date segmentEnd = dayEnd.before(exitTime) ? dayEnd : exitTime; long segmentMinutes = (segmentEnd.getTime() - cursor.getTime()) / 60000; BigDecimal dayCharge = rule.getUnitPrice() .multiply(BigDecimal.valueOf(Math.ceil(segmentMinutes / 60.0))); if (dayCharge.compareTo(rule.getMaxDailyCharge()) > 0) { dayCharge = rule.getMaxDailyCharge(); } totalCharge = totalCharge.add(dayCharge); cursor = dayEnd; } return totalCharge; }这个算法把每一段的计费单独封顶,而不是等累计到总额后再整体封顶。举个例子,某车周五晚上10点入场,周六中午12点出场,收费系统应该按两个自然天的段分别计算,如果规则单日封顶20元,周五晚上虽然只停了2小时但依然产生费用,周六再按当天计算。这才是真实停车场系统的计费逻辑。如果用总时长加整体封顶的错误方法,跨天停车会少收很多钱。这段代码里用到的BigDecimal是为了避免浮点金额的精度误差,这一点后面在避坑章节还要再提。
4.3 出场时更新状态:事务控制
生成订单和更新入场记录状态必须在一个事务里完成,否则可能出现订单生成了但入场记录还是“在场”状态,导致车还没出场就能再次触发一次计费。
@Transactional public ExitResult exit(String plateNumber, String exitDeviceId) { EntryRecord entry = entryRecordMapper.findByPlateAndStatus(plateNumber, 0); if (entry == null) { // 查不到入场记录,走手动入场补充流程 return ExitResult.noEntryRecord(plateNumber); } ChargeRule rule = chargeRuleMapper.findByCarType(entry.getCarType()); BigDecimal amount = computeCharge(entry, new Date(), rule); // 生成出场订单 ExitOrder order = new ExitOrder(); order.setEntryId(entry.getId()); order.setPlateNumber(plateNumber); order.setAmount(amount); order.setExitTime(new Date()); order.setStatus(0); exitOrderMapper.insert(order); // 更新入场记录状态 entry.setStatus(1); entry.setExitOrderId(order.getId()); entryRecordMapper.updateStatusById(entry); return ExitResult.success(amount); }@Transactional注解是关键,只要后续更新状态抛异常,订单也会回滚。很多同学会漏掉这个注解,导致数据不一致。这里还涉及一个常见的场景:相机识别到了出场车但系统里没有入场记录,可能是入场时相机没拍到车牌。我在exit方法中返回noEntryRecord标记,前端收到这个标记后展示手动补充入口,让操作员先选车牌再计算费用,这是答辩评委会追问的功能点。
4.4 会员月租车逻辑:固定费用和临时计费怎么共存
会员车和临时车不能混用一套计费逻辑。月租车入场时不识别为会员按临时车放行,出场时也不应该再次自动生成临时费用。我的方案是在入场阶段查出会员后,出场时判断isMember字段,如果为1且会员状态正常,则直接生成金额为0的订单,同时记录会员的月租有效期开始时间和到期时间。会员到期当天系统要把isMember标记改为0,否则会一直免费出场。这个判断放在入场时并不可靠,因为会员可能在车辆在场时过期,我选择在出场时重新查一次会员状态,以出场时间为准。
5. 前端页面与交互设计:控制台、入口监测与异常订单处理
5.1 页面架构:一张控制台页面收拢所有操作
前端页面我不建议堆砌一堆二级菜单,控制台短链接的设计只需要做到:左侧显示当前在场车辆列表,顶部显示今日应收金额、月租车数量和异常订单数,右侧预留一个手动操作区。它的核心价值是值班员低着头只看一个屏幕就能完成主要操作。技术实现上用Bootstrap加jQuery就够了,复杂的前端框架对这个项目是负担。页面展示的车牌号、入场图片、时长这些信息,建议服务端通过接口分页返回。
5.2 车牌号格式化输入与校验
车牌识别一体机识别车牌的准确率并不是100%可靠,所以我需要在手动录入时做输入校验。下面是一段车牌号格式化脚本:
function formatPlate(input) { // 去掉空格和特殊符号,统一转大写 let value = input.value.replace(/\s+/g, '').toUpperCase(); // 简单校验:第一位是省份汉字,第二位是字母,后面是字母数字组合 const provinceChars = '京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼'; if (provinceChars.indexOf(value.charAt(0)) < 0) { alert('车牌省份简称不正确'); return; } if (!/[A-Z]/.test(value.charAt(1))) { alert('车牌第二位必须是字母'); return; } input.value = value; }这个校验的价值在于拦截低级手误,比如输入“京A12345”和“京a12345”,或者输入多了空格导致后台查询不到记录。绿色车牌的新能源车牌是8位字符,上述正则已经能覆盖,不需要特殊适配。
5.3 在进场记录状态轮询与出场按钮
场内车辆列表需要每30秒刷新一次,避免手动刷新标签页。但轮询有个明显的坑:如果页面刷新时恰好有车辆入场,后端返回的列表会重新加载,正在操作的下拉框或被强制重置。我的解决办法是列表刷新只更新表格区域的HTML,不刷新整个页面,用fetch请求仅获取列表接口的返回数据。同时把当前选中的车牌号缓存到一个变量中,刷新后重建选中状态。这个细节看起来很细,但现场值班演示时,刚好有车入场导致正在填写的表单被重置,就是一场事故。
5.4 异常订单手动处理页面
异常订单包括无入场记录的车辆、摄像机重复识别导致出场失败、会员过期仍被识别等。单独设置一个异常订单页面,状态列表展示异常订单的时间、车牌号、异常类型和备注。操作员可以在页面中为异常订单补充入场时间或调整金额。补充完成后,系统会重新计算金额并生成正常的订单。这个页面是答辩时的加分项,因为很多同学的毕业设计里根本没有想到异常链路。
6. 避坑指南:停车场管理系统里最容易翻车的5个细节
6.1 车牌号里的汉字被存成乱码
现象是入场记录里显示“浣?A12345”,数据库字段显示为问号。原因是建表时用了latin1或者连接字符串没有指定utf8mb4。解决方法是统一改所有字符集配置。修改连接URL为jdbc:mysql://localhost:3306/parking?useUnicode=true&characterEncoding=utf8mb4,同时表结构已经定义成utf8mb4,两个位置不能有一个漏掉。如果已经出现乱码数据,需要清掉重录,没有后悔药可吃。
6.2 出场记录比入场记录多了一条
现象是同一辆车出场后,订单列表里出现了两条记录,金额也重复。原因是出场接口被设备重复回调,而我们的代码没有做幂等判断。修复方式是出场接口也要先查status为0的入场记录,如果入场记录已经被标记为已离场,直接返回原订单信息,不再生成新订单。我在4.1小节入场接口做了重复入场过滤,但出场接口同样需要处理,很多同学只处理了入场却漏了出场。
6.3 跨天停车金额明显偏低
现象是晚上8点入场、第二天早上8点出场,按总时长12小时计费,结果只有不到10块钱,明显少收。原因是计算时没有按自然天拆分,导致单日封顶规则只作用在总时长上。解决方法是使用4.2小节的按自然天遍历算法,把每一天的停车时长单独计算再累加。另外要注意的是,endOfDay方法要准确返回当天的23:59:59,不要用“加上24小时”的写法,否则夏令时或毫秒误差会引发边界值异常。
6.4 金额浮点运算出现0.30000000000000004
现象是金额计算后出现一长串小数。原因是用了double或float来存储和计算金额,浮点数的二进制表示天然有精度问题。解决方法是数据库字段用decimal,Java实体类用BigDecimal,计算时用BigDecimal的multiply和add方法,不要用乘法符号。这条规则不仅适用于停车场系统,任何涉及钱的系统都应该遵守。
6.5 设备网络断连导致车辆一直排队
现象是道闸的识别一体机断网后,所有车辆在门口排长队,系统完全无感知。原因是系统与设备的通信是单向依赖,没有做设备心跳检测和离线补偿机制。解决方法是新增一个nvr设备状态表,让设备每30秒上报心跳,如果系统在2分钟内没有收到心跳,就自动将设备状态置为离线,并在前端页面用红色标记提示。离线期间尝试入场的车辆,可以先放行入场并记录本地缓存,等网络恢复后批量提交。这个设计不仅真实,而且能给答辩增加不少技术深度。
7. 进阶技巧:用异步对账机制验证系统数据的准确性
停车场管理系统做完基础功能后,不要急着提交报告,先用对账机制把系统的数据自检一遍。写一个定时任务,每5分钟统计一次今日应收金额,再和订单表里的sum(amount)对一次,不一致就告警。这个自检程序能提前发现很多隐藏的计费问题,比如某条订单没计算金额就生成了。
定时任务用Spring的@Scheduled注解实现,核心逻辑就是两条SQL求差值。再把对账结果输出到一个日志文件里,答辩时打开日志文件给评委看,今天每小时的金额波动曲线,比空口介绍系统功能更有说服力。
另外,还可以做一个更有价值的增强:在出场订单生成时记录计费规则的快照。什么意思?也就是说,订单表里除了保存金额,还保存当时使用的单价、免费时长和封顶金额。这样即使后续管理员修改了计费规则,历史订单的金额依然有据可查。这个做法看似简单,但很多真实的商业系统都忽略了,导致对账时出现“规则已改、旧账无法解释”的死局。
验收前我也建议做一遍完整流程演练:准备两三个车牌号,模拟入场、出场、跨天停车、会员过期、无入场记录五种场景,确认每一条都符合预期。每晚闭园时段再对一遍当日流水,看入场数和出场数是否一致,在场车辆数是否等于上一次输出的在场数加本次差额。这是我在那个模拟项目X里踩过坑后养成的习惯——先信代码,再做测试,代码不会骗人,但写代码的人会骗自己。
希望这一套从表结构到计费逻辑的笔记,能给正在做停车场管理系统的同学省下几个通宵。祝你的答辩一切顺利。
本文还有配套的精品资源,点击获取