ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java Web停车场管理系统实战:Spring Boot+MyBatis从建表到计费

Java Web停车场管理系统实战:Spring Boot+MyBatis从建表到计费 简介这是一套面向Java Web初学者与课程设计/毕业设计开发者的完整停车场管理系统项目资料围绕B/S架构下的车位管理、用户认证、在线预约、支付集成与报表统计等核心业务展开帮助读者理解从需求分析到编码落地的完整流程。压缩包共16个文件约40.51MB包含7个png项目截图、3个docx论文与开题报告、2个mp4项目辅导视频、1个pptx答辩演示、1个zip源代码包以及1个sql脚本和1个db数据库文件覆盖源码、数据库、文档与录屏四类学习素材。目前已有1632人学习下载。读者可借助源码与数据库脚本快速还原运行环境通过辅导视频掌握部署、数据库创建与项目启动步骤并结合论文、任务书与答辩PPT梳理设计思路与实现细节适合作为课程设计参考或毕业设计模板也可用于练习Servlet、Spring、MyBatis等技术的综合应用。1. 停车场管理系统为什么值得用 Java Web 重写一遍很多做毕设或接私活的朋友第一次接触「基于 Web 停车场管理系统的设计与实现」时脑子里想的都是页面好不好看结果真正卡住的地方全在后端车牌识别记录怎么和车位状态对上、进出场时间差怎么算钱、并发抬杆时数据库锁不锁得住。这套系统本质是一个典型的 Java Web 企业级项目用 Spring Boot MyBatis 做后端MySQL 存业务数据前端用 Thymeleaf 或 Vue 渲染核心业务是车辆进出登记、车位分配、计费结算和报表统计。它适合三类人一是需要完整项目练手的 Java 初学者二是要做课程设计或毕业设计的学生三是想拿一个能讲清楚业务闭环的 Web 项目去面试的求职者。标题里提到的 JAVA 源码、数据库 SQL、论文和视频其实对应的是同一套东西的四种交付形态真正值钱的是你能不能把「车进场到出场扣费」这条链路自己跑通。2. 先把业务模型定下来车位、车辆、订单三张表怎么设计2.1 为什么表结构决定了这个项目能不能讲清楚停车场管理系统的复杂度不在页面在数据关系。我见过太多人一上来就建一张car_record大表把车牌、车位号、进场时间、出场时间、金额全塞进去写到后面发现月卡用户和临时用户逻辑打架报表也统计不出来。正确的做法是先拆出三个核心实体车位parking_space、车辆vehicle、停车订单parking_order。车位有状态空闲、占用、预约、维修车辆有类型临时车、月卡、VIP订单记录一次完整的进出场行为。这三者之间的关系是一辆车可以有多条订单一个车位可以被多辆车在不同时间段占用订单是连接车辆和车位的中间表。数据库 SQL 这块热词里很多人搜「数据库 sql 基础知识」和「dbeaver 连接 sql 数据库不显示」说明不少人在建表和连库阶段就卡住了。我一般会先用 DBeaver 或 Navicat 连上 MySQL把建表语句跑一遍确认字符集是 utf8mb4否则车牌里的特殊字符会乱码。下面是我常用的三张核心表结构字段名和类型可以直接抄。-- 车位表记录每个车位的基本信息和当前状态 CREATE TABLE parking_space ( space_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 车位主键, space_no VARCHAR(20) NOT NULL UNIQUE COMMENT 车位编号如 A-001, area VARCHAR(50) DEFAULT 默认区域 COMMENT 所属区域, status TINYINT DEFAULT 0 COMMENT 0空闲 1占用 2预约 3维修, hourly_rate DECIMAL(6,2) DEFAULT 5.00 COMMENT 该车位每小时费率, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 车辆表登记常驻车辆临时车可以不预先登记 CREATE TABLE vehicle ( vehicle_id INT PRIMARY KEY AUTO_INCREMENT, plate_no VARCHAR(15) NOT NULL UNIQUE COMMENT 车牌号, owner_name VARCHAR(30) COMMENT 车主姓名, phone VARCHAR(20) COMMENT 联系电话, v_type TINYINT DEFAULT 0 COMMENT 0临时车 1月卡 2VIP, balance DECIMAL(10,2) DEFAULT 0.00 COMMENT 账户余额月卡和VIP用, valid_until DATE COMMENT 月卡有效期 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 停车订单表一次进场到出场生成一条记录 CREATE TABLE parking_order ( order_id BIGINT PRIMARY KEY AUTO_INCREMENT, plate_no VARCHAR(15) NOT NULL COMMENT 车牌号冗余存储方便查询, space_id INT COMMENT 分配的车位ID, entry_time DATETIME NOT NULL COMMENT 进场时间, exit_time DATETIME COMMENT 出场时间未出场为空, duration_min INT COMMENT 停车时长分钟, fee DECIMAL(10,2) COMMENT 应收金额, pay_status TINYINT DEFAULT 0 COMMENT 0未支付 1已支付 2已减免, INDEX idx_plate (plate_no), INDEX idx_entry (entry_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这三张表的逻辑说明parking_space的status字段是并发控制的关键车辆进场时要把对应车位从 0 改成 1出场时改回 0。vehicle表里v_type决定计费策略临时车按小时算月卡和 VIP 走余额扣减。parking_order里我特意冗余了plate_no因为报表查询经常按车牌聚合每次都 join 车辆表会慢。参数上hourly_rate我设了默认 5 元实际项目里可以按区域差异化配置比如地库 5 元、地面 3 元。索引idx_plate和idx_entry是必须的否则订单表上百万行之后按车牌查历史记录会全表扫描。2.2 用 Spring Boot MyBatis 搭最小可运行骨架选型理由很直接Spring Boot 省掉 SSM 那套 XML 配置MyBatis 比 JPA 更贴近 SQL方便你手写复杂查询也方便面试时讲清楚每条 SQL 干了什么。热词里「idea2024 版本创建 web 项目」和「spring boot mybatis 的 java 开源多商户跨境商城源码下载」说明很多人卡在工程创建和依赖引入。我一般用 Spring Initializr 生成骨架依赖勾选 Spring Web、MyBatis Framework、MySQL Driver、Lombok。生成后先改application.yml把数据库连接配好。spring: datasource: url: jdbc:mysql://localhost:3306/parking_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.parking.entity configuration: map-underscore-to-camel-case: true配置里serverTimezoneAsia/Shanghai不加会报时区错误这是新手最常见的翻车点。map-underscore-to-camel-case: true让数据库的space_no自动映射到 Java 的spaceNo省掉大量 resultMap 配置。接下来写 Mapper 接口和 XML以车位查询为例。Mapper public interface ParkingSpaceMapper { // 查询所有空闲车位用于进场分配 ListParkingSpace selectFreeSpaces(); // 乐观锁更新车位状态防止并发重复分配 int occupySpace(Param(spaceId) Integer spaceId, Param(oldStatus) Integer oldStatus); }select idselectFreeSpaces resultTypecom.example.parking.entity.ParkingSpace SELECT * FROM parking_space WHERE status 0 ORDER BY space_no LIMIT 10 /select update idoccupySpace UPDATE parking_space SET status 1 WHERE space_id #{spaceId} AND status #{oldStatus} /updateoccupySpace这条 SQL 是并发控制的核心WHERE status #{oldStatus}保证只有车位还是空闲时才能占用返回影响行数为 0 就说明被别人抢先了业务层要重新分配。参数oldStatus传 0spaceId传具体车位 ID。这种写法比SELECT ... FOR UPDATE轻量适合停车场这种并发不算极端的场景。3. 进出场与计费逻辑从抬杆到扣费的完整链路3.1 进场流程的三个关键判断车辆进场不是简单插一条记录。我一般按这个顺序处理先判断车牌是否已在vehicle表登记没登记就自动创建一条临时车记录然后查该车牌有没有未出场的订单有就说明上次没正常出场需要先补录出场最后分配车位用selectFreeSpaces拿空闲车位再用occupySpace抢占抢占失败就重试下一个。这三步任何一步漏掉后面都会出脏数据。热词里「java 判断字符串中是否不是字母和数字」其实和车牌校验有关车牌包含汉字、字母和数字不能用简单的正则一刀切我一般只做长度和非空校验把合法性交给业务规则。Service public class EntryService { Autowired private VehicleMapper vehicleMapper; Autowired private ParkingSpaceMapper spaceMapper; Autowired private ParkingOrderMapper orderMapper; Transactional(rollbackFor Exception.class) public String handleEntry(String plateNo) { // 1. 车辆不存在则登记为临时车 Vehicle vehicle vehicleMapper.selectByPlate(plateNo); if (vehicle null) { vehicle new Vehicle(); vehicle.setPlateNo(plateNo); vehicle.setVType(0); vehicleMapper.insert(vehicle); } // 2. 检查是否有未出场订单 ParkingOrder last orderMapper.selectUnfinishedByPlate(plateNo); if (last ! null) { return 该车牌存在未出场记录请先处理订单 last.getOrderId(); } // 3. 抢占空闲车位 ListParkingSpace freeList spaceMapper.selectFreeSpaces(); for (ParkingSpace space : freeList) { int rows spaceMapper.occupySpace(space.getSpaceId(), 0); if (rows 1) { ParkingOrder order new ParkingOrder(); order.setPlateNo(plateNo); order.setSpaceId(space.getSpaceId()); order.setEntryTime(new Date()); orderMapper.insert(order); return 进场成功车位 space.getSpaceNo(); } } return 车位已满请稍后; } }这段代码的逻辑说明Transactional保证三步要么全成功要么全回滚避免出现「订单建了但车位没占上」的脏数据。参数上plateNo是入口参数来自前端表单或车牌识别设备。occupySpace返回 1 表示抢占成功返回 0 就继续循环下一个车位。这里有个细节selectFreeSpaces我加了LIMIT 10避免一次查出几千个车位实际项目里可以按区域分批查。3.2 出场计费时长、费率、优惠三件事出场逻辑比进场复杂因为要算钱。核心公式是停车时长 出场时间 - 进场时间费用 时长 × 费率再根据车辆类型做减免。临时车全额月卡在有效期内免费VIP 打八折。我一般把计费逻辑单独抽一个FeeCalculator类方便单元测试也方便面试时讲清楚策略模式。Component public class FeeCalculator { // 计算停车费用返回应收金额 public BigDecimal calc(ParkingOrder order, Vehicle vehicle, BigDecimal hourlyRate) { long minutes (order.getExitTime().getTime() - order.getEntryTime().getTime()) / 60000; // 不足一小时按一小时算这是行业惯例 long hours (minutes 59) / 60; BigDecimal fee hourlyRate.multiply(BigDecimal.valueOf(hours)); // 月卡有效期内免费 if (vehicle.getVType() 1 vehicle.getValidUntil() ! null !vehicle.getValidUntil().before(new Date())) { return BigDecimal.ZERO; } // VIP 八折 if (vehicle.getVType() 2) { fee fee.multiply(new BigDecimal(0.8)); } return fee.setScale(2, RoundingMode.HALF_UP); } }参数说明minutes是停车分钟数hours用(minutes 59) / 60实现向上取整停 61 分钟算 2 小时。hourlyRate从车位表带过来不同车位可以不同价。setScale(2, RoundingMode.HALF_UP)保证金额保留两位小数且四舍五入。这里有个血泪经验时间差一定要用getTime()拿毫秒再除直接用Date相减在跨天时会出问题。出场时还要把车位状态改回 0订单补上exit_time、duration_min、fee这三步同样要放在一个事务里。4. 避坑与排查这套系统最容易翻车的五个地方4.1 车牌识别记录和手动录入对不上现象摄像头识别到「京A12345」手动录入变成「京A1234S」两条记录对不上车辆出场时找不到进场订单。原因车牌识别设备的字符集和数据库字符集不一致或者前端输入框没做 trim。解决数据库统一用 utf8mb4前端提交前plateNo.trim().toUpperCase()识别设备回调的接口里加一层车牌格式校验把容易混淆的字符0 和 O、1 和 I做归一化处理。4.2 并发进场时同一车位被分配两次现象两辆车几乎同时进场系统给它们分了同一个车位。原因selectFreeSpaces和occupySpace之间有时间窗口两个线程都查到了同一个空闲车位。解决occupySpace用WHERE status 0做乐观锁返回 0 就重试。如果并发量再大可以在selectFreeSpaces上加FOR UPDATE但停车场场景乐观锁足够。注意别用synchronized锁整个方法那样吞吐量会掉得很难看。4.3 月卡到期时间判断用错时区现象月卡明明还没到期系统却按临时车收费。原因valid_until存的是 DATE 类型Java 里new Date()带时分秒比较时当月卡到期日当天 00:00 就判定过期。解决比较前把当前时间归零到当天或者把valid_until存成 DATETIME 并设为当天 23:59:59。我一般用LocalDate.now()和validUntil.toLocalDate()比较避开时分秒的坑。4.4 订单表数据量大了之后报表查询超时现象系统跑三个月后报表页面加载要十几秒。原因parking_order表没有按时间分区SELECT * FROM parking_order WHERE entry_time BETWEEN ...全表扫描。解决给entry_time加索引报表查询只查需要的字段别SELECT *。数据量再大就按月分表或者把历史订单归档到parking_order_history表。热词里「linux web 缓存」和「web 服务器安全」也提醒我们报表可以加一层 Redis 缓存但停车场项目一般用不上索引优化就够了。4.5 数据库连接池配置太小导致高峰期请求排队现象早晚高峰时接口响应慢日志里出现Connection is not available。原因Spring Boot 默认 HikariCP 最大连接数 10停车场高峰期并发可能到几十。解决在application.yml里调大spring.datasource.hikari.maximum-pool-size一般设 20 到 50同时把connection-timeout设成 3000 毫秒避免请求无限等待。注意别设太大数据库本身也有连接上限MySQL 默认 151留点余量。5. 把项目讲出彩三个进阶技巧和验证方法5.1 用状态机管订单别用一堆 if-else订单状态有「已进场」「已出场待支付」「已支付」「已取消」如果每个操作都写 if-else 判断当前状态代码会变成一团乱麻。我一般用枚举加状态转移表把合法转移列出来非法转移直接抛异常。这样面试时你可以说「我用状态机保证了订单流转的合法性」比说「我写了一堆 if」高一个档次。public enum OrderStatus { ENTERED, EXITED, PAID, CANCELLED; // 定义合法转移进场-出场-支付进场-取消 private static final MapOrderStatus, SetOrderStatus TRANSFER Map.of( ENTERED, Set.of(EXITED, CANCELLED), EXITED, Set.of(PAID), PAID, Set.of(), CANCELLED, Set.of() ); public boolean canTransferTo(OrderStatus target) { return TRANSFER.getOrDefault(this, Set.of()).contains(target); } }参数说明TRANSFER用Map.of定义每个状态能去往哪些状态canTransferTo返回布尔值。业务层在更新订单前先调这个方法不合法就抛业务异常。这样订单状态永远不会出现「已支付又变回已进场」这种脏数据。5.2 用单元测试验证计费逻辑别靠手点页面计费是这套系统里最容易出错也最该测的部分。我一般用 JUnit 5 写几个边界用例停 1 分钟、停 60 分钟、停 61 分钟、月卡当天到期、VIP 打折。跑一遍测试比手动点十次页面靠谱得多。验证方法上你可以把FeeCalculator的calc方法单独拿出来测不依赖数据库这样测试跑得飞快。Test void testFeeCalc() { FeeCalculator calc new FeeCalculator(); ParkingOrder order new ParkingOrder(); order.setEntryTime(Date.from(Instant.now().minusSeconds(3660))); // 61分钟前 order.setExitTime(new Date()); Vehicle temp new Vehicle(); temp.setVType(0); // 61分钟按2小时算费率5元应收10元 assertEquals(new BigDecimal(10.00), calc.calc(order, temp, new BigDecimal(5.00))); }这个测试用例验证了向上取整逻辑minusSeconds(3660)是 61 分钟期望费用 10 元。参数上hourlyRate传 5.00vType传 0 表示临时车。跑通这个测试计费的核心逻辑就稳了。5.3 论文和视频怎么用才不浪费时间标题里提到的论文和视频我的习惯是论文只看第三章「系统设计」和第四章「系统实现」这两章有 ER 图和流程图能帮你快速理清表关系视频只看环境搭建和项目部署那几集别从头看到尾太耗时间。真正要自己动手的是把 SQL 跑一遍、把 Mapper 写一遍、把计费逻辑测一遍。我见过太多人论文抄了一堆代码一行没跑答辩时被问「你这个车位分配怎么防并发」直接卡住。记住这套系统的价值不在文档在你能不能当着面试官的面把进场到出场这条链路讲清楚并且说出你在哪里踩过坑、怎么解决的。希望帮到你。本文还有配套的精品资源点击获取
返回列表