news 2026/10/8 5:01:47

SpringBoot+MySQL智能停车场管理系统:车位预约、计费与权限控制实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+MySQL智能停车场管理系统:车位预约、计费与权限控制实战

简介:这是一套面向Java Web初学者与课程设计开发者的智能停车场管理系统源码,基于SpringBoot框架与MySQL数据库构建,适用于商业综合体、写字楼及住宅小区等停车场景。系统围绕车位预约、停车费动态计算、车辆进出记录管理、多级用户权限控制以及运营数据统计分析等模块展开,能够帮助读者理解从需求到落地的完整业务闭环。压缩包共77个文件,约3.29MB,以Java源码、XML配置、JSP页面、properties配置、JS脚本及PNG图片等为主,另含jar依赖与说明文档,目录结构清晰,便于按模块查阅与二次开发。目前已有92人学习下载。对于正在准备毕业设计或希望掌握SpringBoot实战的读者而言,该资源提供了可直接运行的工程骨架、数据库交互示例与权限控制思路,既能作为课程作业参考,也可用于梳理停车管理类系统的表结构设计与业务分层逻辑。

1. 智能停车场管理系统:从车位预约到费用结算,一套 SpringBoot + MySQL 方案能扛住什么

商业综合体地下三层、写字楼早晚高峰、住宅小区临停混行,这三类场景对停车系统的诉求完全不同:综合体怕高峰期入口堵死,写字楼怕固定车位被临停占用,小区怕外来车辆赖着不走。一套基于 SpringBoot 和 MySQL 的智能停车场管理系统,核心要解决的就是车位预约、停车费计算、车辆进出记录、用户权限控制和数据统计分析这五件事。它适合有 Java 后端基础、想做一个能真实跑起来的业务系统的开发者,也适合物业信息化团队做二次开发。下面我按自己落地过的思路,把表结构、预约锁位、计费规则、权限模型和统计口径一层层拆开讲,能抄的地方直接给代码和参数。

2. 先定表结构再写代码:五张核心表撑起车位预约与进出记录

2.1 为什么表结构决定后面 80% 的返工

我见过太多人上来就写 Controller,结果做到计费发现订单表没有入场时间字段,做到权限发现用户和角色揉在一张表里。停车场系统的数据模型其实不复杂,但字段设计错了,后面每加一个功能都要改表。核心就五张表:用户表、车位表、预约表、进出记录表、计费规则表。用户表存账号和角色,车位表存车位编号、区域、状态,预约表关联用户和车位并记录预约时段,进出记录表记录每次进出的时间和车牌,计费规则表存不同车型和时段的费率。

这里有个容易忽略的点:车位状态不要只用一个字段表示。我一般会拆成status(空闲/占用/预约/维修)和current_plate(当前车牌)两个字段,因为预约中的车位和已停车的车位在业务上要区分对待,前者可以被超时释放,后者不能。

2.2 建表 SQL 与字段说明

-- 用户表:角色用枚举值区分,避免多表关联拖慢登录查询 CREATE TABLE `sys_user` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL COMMENT '登录名', `password` VARCHAR(100) NOT NULL COMMENT 'BCrypt加密后的密码', `role` TINYINT NOT NULL DEFAULT 3 COMMENT '1管理员 2物业 3业主 4临停', `phone` VARCHAR(20) DEFAULT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 车位表:status 和 current_plate 分开,预约和占用是两种状态 CREATE TABLE `parking_spot` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `spot_no` VARCHAR(20) NOT NULL COMMENT '车位编号如 B1-023', `area` VARCHAR(20) NOT NULL COMMENT '区域 B1/B2/B3', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0空闲 1已预约 2已占用 3维修', `current_plate` VARCHAR(10) DEFAULT NULL COMMENT '当前车牌', `version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (`id`), UNIQUE KEY `uk_spot_no` (`spot_no`), KEY `idx_area_status` (`area`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 预约表:记录预约时段,用于超时释放 CREATE TABLE `reservation` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `spot_id` BIGINT NOT NULL, `plate` VARCHAR(10) NOT NULL, `start_time` DATETIME NOT NULL, `end_time` DATETIME NOT NULL, `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待入场 1已入场 2已取消 3已超时', PRIMARY KEY (`id`), KEY `idx_spot_time` (`spot_id`, `start_time`, `end_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 进出记录表:每次进出都插一条,出场时回填费用 CREATE TABLE `access_record` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `plate` VARCHAR(10) NOT NULL, `spot_id` BIGINT DEFAULT NULL, `entry_time` DATETIME NOT NULL, `exit_time` DATETIME DEFAULT NULL, `fee` DECIMAL(10,2) DEFAULT NULL COMMENT '出场时计算', PRIMARY KEY (`id`), KEY `idx_plate_entry` (`plate`, `entry_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 计费规则表:按时段和车型配置费率 CREATE TABLE `billing_rule` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `vehicle_type` TINYINT NOT NULL COMMENT '1小型车 2大型车', `free_minutes` INT NOT NULL DEFAULT 30 COMMENT '免费时长', `hourly_rate` DECIMAL(6,2) NOT NULL COMMENT '每小时费率', `daily_cap` DECIMAL(8,2) NOT NULL COMMENT '每日封顶', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

字段说明:parking_spot的version字段是给乐观锁用的,后面预约抢位会用到。access_record的fee字段允许为空,因为入场时还不知道停多久。billing_rule的daily_cap是封顶价,防止停一天比停一小时还便宜这种反直觉情况。

2.3 索引怎么加才不拖慢查询

停车场系统最频繁的查询是「按车牌查当前是否在场」和「按区域查空闲车位」。前者走idx_plate_entry联合索引,后者走idx_area_status。注意idx_area_status把area放前面,因为区域筛选的区分度比状态高。如果反过来,MySQL 可能走全表扫描。另外reservation表的idx_spot_time是为了查某个车位在某个时段是否已被预约,这个查询在预约接口里每次都会执行。

3. 车位预约的并发控制:乐观锁 + 时段重叠校验

3.1 为什么不能只用数据库行锁

预约接口的核心问题是并发抢同一个车位。用SELECT ... FOR UPDATE行锁能解决,但停车场系统读多写少,行锁会阻塞其他查询。我一般用乐观锁:更新时带version条件,更新影响行数为 0 就说明被别人抢了,直接返回失败让用户重选。这样大部分请求不阻塞,只有真正冲突的那几个才重试。

3.2 预约接口的完整实现

@Service public class ReservationService { @Autowired private ParkingSpotMapper spotMapper; @Autowired private ReservationMapper reservationMapper; @Transactional(rollbackFor = Exception.class) public Result reserve(Long userId, Long spotId, String plate, LocalDateTime start, LocalDateTime end) { // 1. 校验时段是否重叠:同一车位在 start~end 内不能有有效预约 int overlap = reservationMapper.countOverlap(spotId, start, end); if (overlap > 0) { return Result.fail("该时段已被预约"); } // 2. 乐观锁更新车位状态:只有当前是空闲(0)才能改成已预约(1) int updated = spotMapper.updateStatusWithVersion( spotId, 0, 1, plate); if (updated == 0) { return Result.fail("车位已被抢占,请重新选择"); } // 3. 写入预约记录 Reservation r = new Reservation(); r.setUserId(userId); r.setSpotId(spotId); r.setPlate(plate); r.setStartTime(start); r.setEndTime(end); r.setStatus(0); reservationMapper.insert(r); return Result.ok("预约成功"); } }

对应的 Mapper SQL:

<!-- 乐观锁更新:version 不匹配则影响行数为 0 --> <update id="updateStatusWithVersion"> UPDATE parking_spot SET status = #{newStatus}, current_plate = #{plate}, version = version + 1 WHERE id = #{spotId} AND status = #{expectStatus} AND version = #{version} </update> <!-- 时段重叠:新预约的 start 小于已有 end,且 end 大于已有 start --> <select id="countOverlap" resultType="int"> SELECT COUNT(*) FROM reservation WHERE spot_id = #{spotId} AND status IN (0, 1) AND start_time &lt; #{end} AND end_time &gt; #{start} </select>

逻辑说明:先查重叠再更新状态,两步都在同一个事务里。重叠判断用的是标准区间相交条件start < end' AND end > start',注意不能写成BETWEEN,因为BETWEEN是闭区间,边界相接会被误判为重叠。参数上status IN (0,1)表示只检查待入场和已入场的预约,已取消和已超时的不算。

3.3 超时释放怎么做

预约了不来是常态。我一般用一个定时任务每 5 分钟扫一次reservation表,把status=0且start_time超过当前时间 15 分钟的预约改成status=3(已超时),同时把对应车位状态改回空闲。这个 15 分钟是可配的,写在配置文件里。注意定时任务要加分布式锁,否则多实例部署时会重复释放。

4. 停车费计算:分时段费率与封顶价的实现细节

4.1 计费规则怎么设计才不扯皮

停车费计算最容易出纠纷。我的经验是:规则要简单到用户能自己算出来。常见做法是免费 30 分钟,之后每小时 5 元,不足一小时按一小时算,24 小时封顶 40 元。跨天的话按自然日重新计算。这里有个坑:如果用户停了 25 小时,不能简单按 40 元封顶,而要拆成第一天 40 元加第二天按小时算。所以计费函数要按天分段。

4.2 计费核心代码

public BigDecimal calculateFee(LocalDateTime entry, LocalDateTime exit, BillingRule rule) { long totalMinutes = Duration.between(entry, exit).toMinutes(); // 免费时长内不收费 if (totalMinutes <= rule.getFreeMinutes()) { return BigDecimal.ZERO; } // 按自然日拆分:跨天要分别计算每天的费用再累加 BigDecimal total = BigDecimal.ZERO; LocalDateTime cursor = entry; while (cursor.isBefore(exit)) { // 当天结束时间:要么是次日零点,要么是出场时间 LocalDateTime dayEnd = cursor.toLocalDate().plusDays(1).atStartOfDay(); if (dayEnd.isAfter(exit)) { dayEnd = exit; } long minutes = Duration.between(cursor, dayEnd).toMinutes(); // 不足一小时按一小时算,向上取整 long hours = (minutes + 59) / 60; BigDecimal dayFee = rule.getHourlyRate() .multiply(BigDecimal.valueOf(hours)); // 当天费用不超过封顶价 if (dayFee.compareTo(rule.getDailyCap()) > 0) { dayFee = rule.getDailyCap(); } total = total.add(dayFee); cursor = dayEnd; } return total; }

逻辑说明:(minutes + 59) / 60是向上取整的整数除法,比用Math.ceil更直观。按天拆分的循环里,cursor每次跳到当天结束时间,直到追上出场时间。参数上freeMinutes只在第一天生效还是每天都生效,取决于业务约定,我一般只在第一天生效,因为免费时长是给临时办事的人用的,不是给过夜车用的。

4.3 出场结算的接口设计

出场时先查access_record里该车牌最近一条exit_time为空的记录,拿到entry_time,再查计费规则,算出费用后回填fee和exit_time,同时把车位状态改回空闲。这里要注意:如果车牌没有入场记录,说明是异常出场,要记录日志并人工处理,不能直接放行。

5. 权限控制与数据统计:角色隔离和报表口径

5.1 四级角色的权限边界

系统里我设了四种角色:管理员能看所有数据和改配置,物业能看本区域数据和手动放行,业主能预约和查自己的记录,临停只能缴费出场。实现上用 Spring Security 加注解,在方法上标@PreAuthorize("hasRole('ADMIN')")。注意角色不要用字符串硬编码,用枚举,否则改角色名要全局搜索替换。

5.2 数据统计的 SQL 口径

统计报表一般要三个数:今日入场车次、当前在场车辆数、今日收入。这三个数口径要统一,否则对不上账。

-- 今日入场车次:按 entry_time 落在今天算 SELECT COUNT(*) FROM access_record WHERE entry_time >= CURDATE() AND entry_time < CURDATE() + INTERVAL 1 DAY; -- 当前在场:exit_time 为空的就是还在场 SELECT COUNT(*) FROM access_record WHERE exit_time IS NULL; -- 今日收入:按 exit_time 落在今天且 fee 不为空算 SELECT IFNULL(SUM(fee), 0) FROM access_record WHERE exit_time >= CURDATE() AND exit_time < CURDATE() + INTERVAL 1 DAY AND fee IS NOT NULL;

注意收入按exit_time算而不是entry_time,因为费用是出场时才产生的。如果按入场时间算,跨天停车的收入会记到前一天,和实际收款对不上。这是血泪经验,我第一版就踩过这个坑。

6. 避坑与排查:预约、计费、权限里最容易翻车的五件事

6.1 预约成功但车位显示空闲

现象:用户预约后刷新页面,车位还是空闲状态。原因:预约接口更新了车位状态,但前端缓存了旧数据,或者更新事务还没提交就被查询了。解决:预约成功后前端强制刷新车位列表,后端在更新后加@Transactional保证提交后再返回。如果用了 Redis 缓存,更新数据库后要删缓存,不能只更新缓存。

6.2 计费结果比手工算的多一块钱

现象:用户停 1 小时 1 分钟,系统收 2 小时费用,用户投诉。原因:向上取整逻辑对边界处理不对,或者免费时长没有从总时长里扣除。解决:先扣免费时长再算小时数,(totalMinutes - freeMinutes + 59) / 60。另外确认Duration.between返回的是分钟数,如果入场和出场时间有秒级差异,要统一取整到分钟。

6.3 权限注解不生效

现象:加了@PreAuthorize但普通用户还是能访问管理员接口。原因:Spring Security 配置里没开@EnableGlobalMethodSecurity(prePostEnabled = true),或者角色前缀没加ROLE_。解决:检查配置类注解,确认hasRole('ADMIN')对应的权限是ROLE_ADMIN,数据库里存的角色值要和代码里一致。

6.4 定时释放任务重复执行

现象:多实例部署后,同一个超时预约被释放了两次,日志里出现重复记录。原因:定时任务没有加锁,两个实例同时扫到同一条数据。解决:用数据库行锁或 Redis 分布式锁,抢到锁的实例才执行。更简单的做法是更新时加状态条件,UPDATE reservation SET status=3 WHERE id=? AND status=0,影响行数为 0 就说明已被处理。

6.5 统计报表数字对不上

现象:今日入场车次和当前在场车辆数加起来不等于总记录数。原因:入场车次按entry_time算,在场数按exit_time IS NULL算,两个口径的时间范围不同。解决:明确每个指标的定义并写进文档。入场车次是流量指标,在场数是存量指标,本来就不该相加。报表页面上要标注口径说明,避免运营人员误解。

7. 把系统跑稳之后,我习惯用这三个指标验证它

系统上线只是开始,真正要关注的是它稳不稳。我一般盯三个指标:预约成功率、计费准确率、接口平均响应时间。预约成功率低于 95% 说明并发冲突太多,要么加车位要么优化锁粒度。计费准确率靠每天抽 10 笔人工核对,连续一周无差异才算过关。响应时间超过 500ms 就要查慢 SQL,停车场系统大部分慢查询都出在统计报表上,加个按天预聚合的汇总表能解决。

-- 预聚合表:每天凌晨跑一次,报表直接查这张表 CREATE TABLE `daily_stat` ( `stat_date` DATE NOT NULL, `entry_count` INT DEFAULT 0, `exit_count` INT DEFAULT 0, `revenue` DECIMAL(12,2) DEFAULT 0, PRIMARY KEY (`stat_date`) );

这个表用定时任务在每天凌晨 1 点跑,把前一天的access_record聚合进去。报表查询从扫几十万行变成查一行,响应时间从秒级降到毫秒级。注意补数据逻辑:如果某天定时任务失败了,要能手动重跑,所以聚合 SQL 要写成先删后插,保证幂等。

我自己的习惯是每次改计费规则前,先拿历史数据跑一遍新旧规则对比,差异超过 1% 就停下来查原因。停车费这东西,多收一块钱用户会投诉,少收一块钱物业会找你,宁可上线前多花半小时验证。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 5:01:46

Agent-Reach 实战:用 CLI 和 Python 构建能触达真实世界的 AI Agent

1. 从零认识 Agent-Reach&#xff1a;它到底解决什么问题第一次看到 Agent-Reach 这个名字&#xff0c;我下意识把它和市面上那些"套壳聊天机器人"归到了一类&#xff0c;直到我把它的定位、关键词和周边生态串起来看&#xff0c;才发现它其实踩在了一个很实在的痛点…

作者头像 李华
网站建设 2026/10/8 4:59:57

Windows UVC摄像头稳定接入指南:C++/C#绕过驱动黑箱

简介&#xff1a;本资源是一份面向嵌入式开发与USB设备驱动初学者的UVC摄像头底层开发实践包&#xff0c;聚焦USB Video Class标准在C/C环境下的驱动实现与视频流控制。压缩包含12个核心文件&#xff0c;以9个C源码&#xff08;如uvc_driver.c、uvc_video.c、uvc_ctrl.c等&…

作者头像 李华
网站建设 2026/10/8 4:59:32

从Jev刷屏到ConfTuner:模型置信度校准与评测新思路

1. 从Jev刷屏说起&#xff1a;一个被忽视的评测盲区最近技术圈被一个叫Jev的东西刷了屏&#xff0c;朋友圈、技术群、甚至一些平时只聊业务的群都在转。我一开始以为是哪个新出的开源模型或者工具链&#xff0c;点进去看了几篇讨论才发现&#xff0c;大家真正在意的其实不是Jev…

作者头像 李华
网站建设 2026/10/8 4:59:28

ponytail:解决命令行长输出难题的高效整理插件

先说结论&#xff1a;如果你搜到这篇东西&#xff0c;大概率不是在找扎头发的教程。ponytail 是一个专门对付“长输出刷屏”的插件&#xff0c;干的事情一句话概括&#xff1a;把命令跑出来的杂乱文本&#xff0c;像扎马尾辫一样收拢成一段一段清爽的摘要。你不需要改变原来怎么…

作者头像 李华
网站建设 2026/10/8 4:57:53

给 Claude Code 装上第二大脑:claude-mem 长期记忆工具原理与实践

用过 Claude Code 的人大概都有过这种体验&#xff1a;前天晚上和它一起把某个模块的重构方案聊得明明白白&#xff0c;连拆几步、改哪几个文件都定了&#xff0c;第二天新开一个会话&#xff0c;它一脸无辜地看着你&#xff0c;好像你们从未见过。你重新讲一遍背景&#xff0c…

作者头像 李华
网站建设 2026/10/8 4:57:14

电动汽车数据集清洗与特征工程:从电池规格到价格预测

简介&#xff1a;这是一份面向数据分析、市场研究及电动汽车行业爱好者的真实电动汽车数据集&#xff0c;围绕2025年销售的3K条记录&#xff0c;覆盖特斯拉、宝马、日产等品牌的电池规格、续航里程、充电方式、价格、产地、安全等级及销量等属性&#xff0c;可用于车型对比、电…

作者头像 李华