简介:这份资源是基于SpringBoot与MySQL开发的智能停车场管理系统完整源码包,面向计算机专业学生、Java初学者及需要课程设计或毕业设计参考的开发者,针对商业综合体、写字楼、住宅小区等场景下的停车难问题,提供车位预约、停车费计算、车辆进出记录管理、用户权限控制与数据统计分析等核心功能。压缩包共77个文件,约3.29MB,以Java源码、XML配置、JSP页面、properties配置、JS脚本及PNG图片等为主,另含jar依赖、md说明与docx文档,目录结构清晰,便于按模块阅读与二次开发。目前已有92人学习下载。系统通过预约机制缓解车位紧张,依据时长与费率动态计费,并借助进出记录支撑运营分析,权限分级则保障了数据安全。对于想理解SpringBoot整合MySQL、MyBatis及JSP视图层开发流程的读者,这份源码可作为功能拆解与项目搭建的实用参考。
1. 从一份停车场管理系统源码说起:它到底能跑通哪些业务
商业综合体的地下车库,早晚高峰最怕两件事:入口排队堵到马路上,出口缴费堵在闸机前。这套基于 SpringBoot 和 MySQL 的智能停车场管理系统,瞄准的就是这两件事——把车位预约、停车费计算、车辆进出记录、用户权限控制、数据统计分析串成一条闭环。它不是玩具级的 CRUD 演示,而是按真实停车场业务建模的完整项目:车位有状态机(空闲/预约/占用/维护),进出场有时间戳和计费规则,权限分管理员、操作员、普通用户三层。适合谁?正在做课程设计或毕业设计的同学,需要一套能讲清业务逻辑的参考实现;也适合刚转 Java 后端、想拿一个完整项目练手的开发者。下面我按「能跑起来 → 能改得动 → 不踩坑」的顺序拆一遍。
2. 环境搭建与数据库设计:把表结构和依赖先钉死
2.1 技术栈选型与版本对齐
这套系统的主干是 SpringBoot + MySQL + MyBatis,前端常见做法是 Thymeleaf 或前后端分离的 Vue。选 SpringBoot 的理由很直接:自动配置省掉大量 XML,内嵌 Tomcat 让部署变成一个 jar 包的事。但版本对齐是第一个坎。SpringBoot 2.x 配 JDK 8,SpringBoot 3.x 强制 JDK 17 且把 javax 换成了 jakarta,很多老教程里的javax.servlet导入会直接编译不过。我一般会先确认三件事:JDK 版本、SpringBoot 版本、MySQL 驱动版本。MySQL 8.0 用com.mysql.cj.jdbc.Driver,5.7 用com.mysql.jdbc.Driver,驱动坐标也不同。
<!-- pom.xml 关键依赖,SpringBoot 2.7 + JDK 8 组合 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <!-- 2.x 最后一版,兼容 JDK 8 --> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> <!-- 8.0 驱动,注意 cj 包路径 --> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> </dependencies>这段依赖里,spring-boot-starter-parent锁定了整套依赖的版本基线,避免手动指定每个库的版本导致冲突。MySQL 驱动选 8.0.33 是因为它同时兼容 MySQL 5.7 和 8.0 服务端,连接串里加serverTimezone=Asia/Shanghai就能绕开时区报错。MyBatis starter 的版本要跟 SpringBoot 主版本匹配,2.3.x 对应 SpringBoot 2.7,用错会出现SqlSessionFactory找不到的启动异常。
2.2 核心表结构与字段含义
停车场系统的数据模型不复杂,但字段设计直接决定计费逻辑能不能写对。核心是四张表:车位表、车辆表、进出记录表、用户表。车位表要有一个status字段做状态机,进出记录表要有entry_time和exit_time两个时间戳,计费就是拿这两个时间差乘以费率。
-- 车位表:status 用枚举值控制状态流转 CREATE TABLE parking_space ( id BIGINT PRIMARY KEY AUTO_INCREMENT, space_no VARCHAR(20) NOT NULL UNIQUE COMMENT '车位编号如 A-001', area VARCHAR(50) COMMENT '所属区域', status TINYINT DEFAULT 0 COMMENT '0空闲 1已预约 2占用 3维护', hourly_rate DECIMAL(10,2) DEFAULT 5.00 COMMENT '每小时费率' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 进出记录表:计费的数据来源 CREATE TABLE entry_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, plate_no VARCHAR(10) NOT NULL COMMENT '车牌号', space_id BIGINT COMMENT '关联车位', entry_time DATETIME NOT NULL, exit_time DATETIME COMMENT '出场时为空表示在场内', fee DECIMAL(10,2) DEFAULT 0.00 COMMENT '结算费用', INDEX idx_plate (plate_no), INDEX idx_entry (entry_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;status用 TINYINT 而不是字符串,是为了查询效率——统计空闲车位时WHERE status = 0走索引比字符串比较快。entry_record上建了两个索引:车牌号索引用于快速查某辆车的历史记录,入场时间索引用于按时间段做统计报表。exit_time允许为空,这个设计很关键——车辆还在场内时这条记录就是「未闭合」状态,出场时才回填时间和费用。如果设计成非空,就得额外加一张在场表,反而复杂。
2.3 配置文件与连接池参数
application.yml里除了数据源,还要注意连接池和 MyBatis 的映射配置。默认的 HikariCP 连接池参数在高并发下需要调,停车场系统虽然并发不高,但入口闸机可能短时间涌入大量请求。
spring: datasource: url: jdbc:mysql://localhost:3306/parking_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 20 # 最大连接数,按并发量调 minimum-idle: 5 # 最小空闲连接 connection-timeout: 30000 # 获取连接超时 30s mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.parking.entity configuration: map-underscore-to-camel-case: true # 下划线转驼峰,省掉大量 resultMapmap-underscore-to-camel-case这个开关打开后,数据库的space_no会自动映射到 Java 的spaceNo,不用每个字段写resultMap。连接串里的useSSL=false在本地开发时避免 SSL 握手警告,生产环境建议开启。maximum-pool-size设 20 是经验值,停车场系统同时在线操作员通常不超过 10 个,留一倍余量足够。
3. 核心业务实现:车位预约、计费与权限控制怎么落地
3.1 车位预约的状态流转与并发控制
车位预约的本质是一个状态机:空闲 → 预约 → 占用 → 空闲。问题出在并发上——两个用户同时看到同一个空闲车位,同时点预约,如果不加锁就会超卖。常见做法有两种:数据库乐观锁和 Redis 分布式锁。这个项目体量不大,用数据库乐观锁就够了。
// 乐观锁方式:更新时校验 status 是否仍为期望值 @Update("UPDATE parking_space SET status = 1, user_id = #{userId} " + "WHERE id = #{spaceId} AND status = 0") int reserveSpace(@Param("spaceId") Long spaceId, @Param("userId") Long userId); // Service 层判断影响行数 public boolean reserve(Long spaceId, Long userId) { int rows = parkingSpaceMapper.reserveSpace(spaceId, userId); if (rows == 0) { throw new BusinessException("车位已被预约,请刷新后重试"); } return true; }WHERE id = #{spaceId} AND status = 0这一句是乐观锁的核心——只有当车位当前确实是空闲状态时更新才会生效。如果另一个线程已经把它改成预约状态,rows就是 0,直接抛业务异常。这种写法不需要显式加锁,靠数据库的行锁保证原子性,比synchronized更可靠,因为后者在集群部署时会失效。参数上,spaceId和userId都要做非空校验,userId从登录态里取,不能从前端传,否则越权预约。
3.2 停车费计算:时间差、费率与封顶规则
计费逻辑看着简单,实际坑最多。基础公式是「出场时间减入场时间,按小时向上取整,乘以费率」。但真实场景有封顶价、免费时长、跨天计费。我一般把计费规则抽成一个独立方法,方便单测。
public BigDecimal calculateFee(LocalDateTime entry, LocalDateTime exit, BigDecimal hourlyRate) { long minutes = Duration.between(entry, exit).toMinutes(); if (minutes <= 30) { return BigDecimal.ZERO; // 30 分钟内免费 } // 向上取整:不足一小时按一小时算 long hours = (long) Math.ceil(minutes / 60.0); BigDecimal fee = hourlyRate.multiply(BigDecimal.valueOf(hours)); BigDecimal cap = BigDecimal.valueOf(50); // 单日封顶 50 元 return fee.min(cap); }Duration.between拿到的是分钟差,Math.ceil(minutes / 60.0)做向上取整——停了 61 分钟按 2 小时算,这是停车场行业惯例。fee.min(cap)实现封顶,避免停一天算出天价。注意minutes / 60.0里的60.0必须是浮点,写成60会触发整数除法,61 分钟会变成 1 小时,少收钱。这个 bug 我在两个项目里都见过,属于血泪经验。费率从车位表里读,不同区域可以设不同价格,VIP 车位贵一些。
3.3 基于角色的权限控制实现
权限分三层:管理员能管所有车位和用户,操作员只能处理进出场和收费,普通用户只能预约和查自己的记录。实现方式常见的是 Spring Security 或自己写拦截器。这个项目用拦截器 + 注解的方式更轻量。
// 自定义注解标记接口所需角色 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequireRole { String[] value(); } // 拦截器里校验 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (handler instanceof HandlerMethod) { RequireRole role = ((HandlerMethod) handler).getMethodAnnotation(RequireRole.class); if (role != null) { String userRole = (String) request.getSession().getAttribute("role"); if (!Arrays.asList(role.value()).contains(userRole)) { response.setStatus(403); return false; } } } return true; }@RequireRole({"ADMIN"})标在方法上,拦截器在preHandle里读注解和 session 里的角色比对。角色存在 session 里而不是每次查库,是为了减少数据库压力,代价是角色变更后需要重新登录才生效。response.setStatus(403)返回禁止访问,前端统一拦截 403 跳转到无权限页面。这里要注意:拦截器要注册到WebMvcConfigurer里并配置拦截路径,否则不生效。
4. 数据统计与报表:把进出记录变成可用指标
4.1 常用统计 SQL 与索引优化
统计分析模块通常要出几个指标:当日车流量、平均停车时长、收入汇总、车位周转率。这些都能从entry_record表聚合出来,但 SQL 写不好会全表扫描。
-- 当日收入汇总:按出场时间过滤,只算已结算的 SELECT DATE(exit_time) AS stat_date, COUNT(*) AS total_orders, SUM(fee) AS total_income, AVG(TIMESTAMPDIFF(MINUTE, entry_time, exit_time)) AS avg_minutes FROM entry_record WHERE exit_time IS NOT NULL AND exit_time >= CURDATE() AND exit_time < CURDATE() + INTERVAL 1 DAY GROUP BY DATE(exit_time);exit_time IS NOT NULL过滤掉还在场内的记录,否则fee是 0 会拉低收入。CURDATE() + INTERVAL 1 DAY这种写法比DATE(exit_time) = CURDATE()好,因为前者能走idx_entry索引,后者对字段做函数运算会导致索引失效。TIMESTAMPDIFF(MINUTE, ...)直接算分钟差,比在 Java 里循环计算快得多。如果数据量大,建议按天做汇总表,定时任务跑一次,报表直接查汇总表。
4.2 报表接口与前端展示对接
后端出统计接口时,返回结构要跟前端图表库对齐。ECharts 通常要xAxis数组和series数组,后端直接组装好能省掉前端转换。
@GetMapping("/stats/daily") public Map<String, Object> dailyStats(@RequestParam String date) { List<DailyStat> list = recordMapper.selectDailyStats(date); List<String> hours = new ArrayList<>(); List<Integer> counts = new ArrayList<>(); for (DailyStat stat : list) { hours.add(stat.getHour() + ":00"); counts.add(stat.getCount()); } Map<String, Object> result = new HashMap<>(); result.put("xAxis", hours); result.put("series", counts); return result; }这个接口按小时维度返回车流量,xAxis是["08:00", "09:00", ...],series是对应数量。前端拿到直接塞给 ECharts 的option就行。参数date要做格式校验,防止 SQL 注入——虽然 MyBatis 的#{}已经预编译了,但日期格式不对会抛异常。常见做法是用@DateTimeFormat注解自动转换,或者手动LocalDate.parse加 try-catch。
5. 避坑与排查:部署和运行中最容易翻车的几个点
5.1 启动报错「方法失败,意外」怎么定位
现象:项目启动时控制台抛出Error creating bean或「方法失败,意外」这类模糊提示。原因通常是依赖冲突或配置缺失。先看完整堆栈最底部的Caused by,那里才是根因。常见的是 MySQL 驱动版本和连接串不匹配——用 8.0 驱动连 5.7 数据库,连接串里没加serverTimezone就会报时区错误。解决:确认驱动版本,连接串补上serverTimezone=Asia/Shanghai。
5.2 中文乱码:从数据库到页面的全链路排查
现象:车位编号或用户名显示成问号。原因可能在三处:数据库字符集、连接串编码、页面编码。解决:数据库建库时指定utf8mb4,连接串加characterEncoding=utf8,页面<meta charset="UTF-8">。三处缺一不可,我一般建库时就写死DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci,省得后面改。
5.3 计费金额出现小数精度问题
现象:费用算出15.000000001这种值。原因是用double做金额运算。解决:所有金额字段用BigDecimal,数据库用DECIMAL(10,2)。BigDecimal的multiply和add都要用,不能用+和*。除法要指定精度和舍入模式,否则除不尽会抛ArithmeticException。
5.4 预约接口并发下超卖
现象:压测时同一个车位被两个人预约成功。原因:查询和更新分两步,中间有时间窗口。解决:用UPDATE ... WHERE status = 0的乐观锁,判断影响行数。不要用「先查再改」的写法,那个在并发下必出问题。
5.5 打包后静态资源 404
现象:本地跑正常,打成 jar 后页面样式丢失。原因:SpringBoot 打包后静态资源路径变了,或者用了绝对路径。解决:静态资源放src/main/resources/static下,引用用相对路径或 Thymeleaf 的@{/css/style.css}。如果前后端分离,Vue 打包产物放static下要配置history模式的路由回退。
6. 进阶技巧:把计费规则做成可配置,以及一次压测验证
6.1 用策略模式解耦计费规则
硬编码的计费逻辑改一次费率就要重新编译,不现实。我一般把计费规则抽成策略接口,不同规则实现不同类,用工厂根据车位类型返回对应策略。
public interface FeeStrategy { BigDecimal calculate(LocalDateTime entry, LocalDateTime exit); } @Component("standard") public class StandardFeeStrategy implements FeeStrategy { public BigDecimal calculate(LocalDateTime entry, LocalDateTime exit) { // 标准费率逻辑 } } @Component("vip") public class VipFeeStrategy implements FeeStrategy { public BigDecimal calculate(LocalDateTime entry, LocalDateTime exit) { // VIP 折扣逻辑 } }@Component("standard")把 bean 注册到容器,工厂类通过applicationContext.getBean(strategyName, FeeStrategy.class)动态获取。这样新增一种计费规则只需要加一个类,不改原有代码。策略名存在车位表的rate_type字段里,查出来直接映射。
6.2 用 JMeter 验证预约接口的并发安全
改完乐观锁后,我会用 JMeter 跑一轮并发测试确认。线程数设 50,循环 10 次,目标接口是预约。预期结果是:成功预约数等于车位总数,不出现超卖。如果成功数大于车位数,说明锁没生效,回去检查 SQL 的WHERE条件是不是漏了status = 0。
| 测试项 | 参数 | 预期结果 |
|---|---|---|
| 线程数 | 50 | 并发模拟 |
| 循环次数 | 10 | 总请求 500 |
| 车位总数 | 20 | 最多成功 20 |
| 成功预约数 | ≤ 20 | 无超卖 |
| 平均响应时间 | < 200ms | 可接受 |
压测时数据库连接池要调大,否则请求会卡在获取连接上,误判为接口慢。maximum-pool-size设成线程数的一半左右比较合适。
6.3 一个我踩过的坑:时区导致计费差 8 小时
有次测试环境计费总是多算 8 小时,查了半天发现是 MySQL 服务端时区是 UTC,Java 应用是东八区,entry_time存进去就差了 8 小时。从那以后我每次配数据源都强制加serverTimezone=Asia/Shanghai,并且在application.yml里设spring.jackson.time-zone=GMT+8。这个坑不报错,只是金额悄悄算错,最难排查。建议在项目启动时打一条日志输出当前时区,方便核对。希望帮到你。
本文还有配套的精品资源,点击获取