资讯动态

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

发布时间:2026/10/8 5:02:00 来源:尧图企业网站定制
简介这是一套面向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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 车位表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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 预约表记录预约时段用于超时释放 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 进出记录表每次进出都插一条出场时回填费用 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 计费规则表按时段和车型配置费率 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段说明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 idupdateStatusWithVersion 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 idcountOverlap resultTypeint 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表把status0且start_time超过当前时间 15 分钟的预约改成status3已超时同时把对应车位状态改回空闲。这个 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 status3 WHERE id? AND status0影响行数为 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% 就停下来查原因。停车费这东西多收一块钱用户会投诉少收一块钱物业会找你宁可上线前多花半小时验证。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑