ARTICLE DETAIL

资讯详情

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

Web停车场管理系统实现:状态机、并发控制与前后端分离设计

Web停车场管理系统实现:状态机、并发控制与前后端分离设计 简介这是一份面向计算机专业毕业设计或课程设计的《基于Web的停车场管理系统》完整资料包包含论文和可运行源码适合需要快速完成相关课题或学习ASP.NET Web开发的学生及开发者。系统设计覆盖前端界面、后端服务器、数据库与通信协议并包含用户管理、停车管理、收费管理、报表统计、系统维护等核心功能模块。包内共132个文件主要包括C#源文件.cs、ASP.NET页面.aspx、HTML/CSS/JS前端文件、JPG/GIF图片以及数据库文件.mdf/.ldf等整体约2.93MB目录结构清晰便于按需查阅和二次开发。已有74人学习下载。借助该资料可深入理解停车场管理系统的设计思路与实现细节快速获得可直接运行的演示项目同时参考配套文档完成论文撰写或答辩准备。1. 从课设标题到可运行的 Web 停车场管理系统先搞清楚你要交付什么拿到「基于Web的停车场管理系统的设计与实现」这个题目绝大多数人第一反应是去找一套现成的源码改一改。但真正到答辩或验收时老师问得最多的不是「代码能不能跑」而是「你为什么要选这个架构」「计费规则怎么设计的」「并发入场时会不会超卖车位」。这篇文要解决的问题就是把这套系统从「能跑」推到「能讲清楚、能改、能扛住问题」。停车场管理系统本质上是典型的 CRUD 加状态机业务车位有「空闲/占用/预订」状态车辆有「入场/在场/离场」生命周期订单有「计费中/已结算」状态。难点不在增删改查而在状态怎么流转、计费怎么算、并发下怎么保证一个车位不被同时分配给两辆车。本文会从表结构设计、核心业务接口、前端交互、部署排错到并发验证把一套可复现的 Web 停车场管理系统完整拆开。这篇文适合三类人正在做课设或毕设、需要在论文里写清楚设计思路的在校生刚接触 Web 全栈开发、想找一个完整业务练手的初级工程师以及需要快速评估这类项目代码质量的面试官或评审。2. 停车场管理系统的表结构设计与技术选型先定状态机再写代码2.1 技术栈选型为什么推荐 Spring Boot Vue MySQL 组合常见的技术选型有四种JSP Servlet MySQL 的传统单体方案、Spring Boot Thymeleaf 的服务端渲染方案、Spring Boot Vue 的前后端分离方案、以及 Python Flask/Django 方案。课程设计里最稳妥的是第三种原因有三点第一前后端分离能在论文里多写一章「前后端交互设计与接口规范」这部分内容是答辩时最容易展开讲的技术点。第二Spring Boot 内嵌 Tomcat部署时只需要一个 JAR 包或 WAR 包不需要单独配置复杂环境。第三Vue 的组件化写法能让前端代码量看起来更充实且数据绑定特性天然适合车位状态这种需要实时刷新的场景。MySQL 依然是首选数据库因为它对事务支持成熟适合订单表和计费流水这类需要强一致性的数据。Redis 在这个项目里不是必需项但如果你想让答辩更有亮点可以用它做车位状态的缓存层。2.2 核心表结构车位表、订单表、用户表的关系建模表结构是整个系统的地基。停车场管理系统至少要四张表停车场信息表支持多停车场扩展、车位表、停车订单表、用户表。如果你做了会员或包月功能还需要一张套餐表。CREATE TABLE t_parking_space ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 车位ID, space_no VARCHAR(20) NOT NULL COMMENT 车位编号如 A-001, area_name VARCHAR(50) DEFAULT NULL COMMENT 区域名称如 A区/B区, status TINYINT NOT NULL DEFAULT 0 COMMENT 0空闲 1占用 2预订 3维修, device_sn VARCHAR(64) DEFAULT NULL COMMENT 关联道闸或摄像头设备编号, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_space_no (space_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车位表; CREATE TABLE t_parking_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 订单ID, order_no VARCHAR(32) NOT NULL COMMENT 订单编号如 PK202505171030001, plate_no VARCHAR(20) NOT NULL COMMENT 车牌号, space_id BIGINT NOT NULL COMMENT 车位ID, entry_time DATETIME NOT NULL COMMENT 入场时间, exit_time DATETIME DEFAULT NULL COMMENT 出场时间, duration_minutes INT DEFAULT 0 COMMENT 停车时长分钟, fee DECIMAL(10,2) DEFAULT 0.00 COMMENT 应收金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 0进行中 1已完成 2已取消, UNIQUE KEY uk_order_no (order_no), KEY idx_plate_no (plate_no), KEY idx_space_id (space_id), CONSTRAINT fk_space_id FOREIGN KEY (space_id) REFERENCES t_parking_space(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT停车订单表;字段设计的核心思路是车位状态冗余在车位表订单记录每次停车行为的完整生命周期。这样查询「当前哪些车位空闲」只需要扫车位表不需要关联订单表做子查询。plate_no建立普通索引而不是唯一索引因为同一辆车在不同时间段会有多条订单记录。order_no建议用「业务前缀 日期 自增序号」的拼接方式比如PK202505171030001。这样做的好处是订单号本身就携带了日期信息在按天统计流水时可以直接用字符串截取省掉一次时间转换。2.3 业务状态机车位与订单的状态流转约束在写任何接口之前要把状态流转规则定死。车位状态有四种空闲、占用、预订、维修。订单状态有三种进行中、已完成、已取消。空闲状态在车辆入场并创建订单后变为占用车辆离场并结算后车位变回空闲订单变为已完成。预订状态是给预约功能的预约超时未入场需要定时任务把车位释放回空闲。维修状态由管理员手动设置维修期间不允许分配。这里有一个很容易被忽略的点车位状态更新和订单创建必须在一个数据库事务里完成。如果先更新车位状态再创建订单第二步失败时车位就会被「锁死」。反过来先创建订单再更新车位状态极端情况下会出现订单存在但车位仍是空闲的数据不一致。3. Web 停车场系统的核心代码实现入场、出场、计费与状态更新3.1 后端实现车辆入场与出场计费的核心代码入场接口是系统中并发压力最大的接口核心逻辑是查询空闲车位、创建订单、更新车位状态。三个操作必须在同一事务中并且要锁住车位记录防止并发重复分配。Transactional(rollbackFor Exception.class) public ParkingOrderVO vehicleEntry(String plateNo) { // 1. 悲观锁锁定一条空闲车位记录 ParkingSpace space parkingSpaceMapper.selectOneFreeSpaceForUpdate(); if (space null) { throw new BizException(500, 当前无空闲车位); } // 2. 防止同一辆车重复入场如摄像头识别两次 int activeCount parkingOrderMapper.countActiveByPlateNo(plateNo); if (activeCount 0) { throw new BizException(500, 该车辆已在场内请勿重复入场); } // 3. 创建订单 ParkingOrder order ParkingOrder.builder() .orderNo(generateOrderNo()) .plateNo(plateNo) .spaceId(space.getId()) .entryTime(LocalDateTime.now()) .status(OrderStatus.PENDING) .build(); parkingOrderMapper.insert(order); // 4. 更新车位状态 space.setStatus(SpaceStatus.OCCUPIED); parkingSpaceMapper.updateById(space); return convertToVO(order); }selectOneFreeSpaceForUpdate是关键FOR UPDATE会对命中的记录加行级排他锁。一起来的两辆车同时执行这条 SQL 时第二辆会阻塞等待第一辆事务提交后继续执行此时空闲车位已经被占用第二辆车会收到「无空闲车位」的提示。这种方式牺牲了一点点性能但保证了车位不会被超卖。出场计费接口的要点在于「先计算费用再更新状态」。费用计算规则建议配置化不要写死在代码里Transactional(rollbackFor Exception.class) public ParkingOrderVO vehicleExit(String plateNo) { ParkingOrder order parkingOrderMapper.selectActiveByPlateNo(plateNo); if (order null) { throw new BizException(500, 未找到该车辆的入场记录); } LocalDateTime exitTime LocalDateTime.now(); long minutes ChronoUnit.MINUTES.between(order.getEntryTime(), exitTime); // 调用计费策略计算金额 BigDecimal fee billingStrategy.calculate(order.getEntryTime(), exitTime); order.setExitTime(exitTime); order.setDurationMinutes((int) minutes); order.setFee(fee); order.setStatus(OrderStatus.FINISHED); parkingOrderMapper.updateById(order); // 释放车位 ParkingSpace space parkingSpaceMapper.selectById(order.getSpaceId()); space.setStatus(SpaceStatus.FREE); parkingSpaceMapper.updateById(space); return convertToVO(order); }billingStrategy是一个策略接口可以有多套实现普通按小时计费、夜间优惠计费、会员折扣计费。写论文时这里可以画一张「策略模式类图」比贴十页 CRUD 代码更有设计感。3.2 前端实现车位状态渲染与入场出场操作的表单交互前端用 Vue 3 写一个车位状态总览面板核心是把后端返回的车位列表渲染成网格不同状态用不同颜色标识。轮询接口每 5 秒刷新一次数据替代 WebSocket 降低成本。// ParkingGrid.vue template el-row :gutter12 el-col :span6 v-forspace in spaceList :keyspace.id div classspace-item :classstatusClass(space.status) div classspace-no{{ space.spaceNo }}/div div classspace-status{{ statusText(space.status) }}/div el-button v-ifspace.status 0 sizesmall typeprimary clickhandleEntry(space) 录入入场/el-button el-button v-else-ifspace.status 1 sizesmall typedanger clickhandleExit(space) 结算出场/el-button /div /el-col /el-row /templatestatusClass方法根据状态值返回对应的 CSS 类名0 对应绿色边框、1 对应红色填充、2 对应黄色斜纹。这一层的视觉反馈直接决定了系统「看起来专不专业」答辩时这部分最容易留下好印象。入场弹窗只需要一个车牌号输入框加上「车牌相当于下单入口」的提示文案。出场弹窗则需要展示入场时间、当前时间、停车时长和预估费用用户确认后调用结算接口。3.3 关键参数配置数据库连接池、时区与金额精度Spring Boot 的application.yml里有三个参数直接影响业务正确性。数据库时区必须设置否则LocalDateTime和 MySQL 之间转换会出现 8 小时偏移导致计费时长错乱spring: datasource: url: jdbc:mysql://localhost:3306/parking_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghaimaximum-pool-size要结合 MySQL 的max_connections来设。如果 MySQL 实例允许 151 个连接而你的 HikariCP 池设了 50其他服务或管理端工具就会拿不到连接。金额字段在数据库层用DECIMAL(10,2)在 Java 层用BigDecimal不要把double用于金额计算。4. Web 停车场管理系统的部署与常见坑点排查4.1 从源码到上线Maven 打包与两种部署路径对比项目写完后要交付的可运行产物有两种形态Spring Boot 的 JAR 包和传统 Web 系统的 WAR 包。TA 在验证时最常用的方式是直接拿到 JAR 包在命令行执行java -jar启动。推荐使用这种方式因为你不需要向对方解释 Tomcat 目录结构。# 1. 确保本地有 JDK 8 和 Maven 3.6 mvn clean package -DskipTests # 2. 打包后的 JAR 在 target 目录下 ls -lh target/*.jar # 3. 启动服务指定外部配置文件可选 java -jar target/parking-system-1.0.0.jar --spring.profiles.activeprod # 4. 验证服务是否正常 curl -X POST http://localhost:8080/api/parking/entry \ -H Content-Type: application/json \ -d {plateNo:粤B12345}如果设备管理员需要在服务器上以服务方式常驻运行可以额外配置 systemd 服务。但对于课设和内部演示前台启动加nohup就足够。4.2 部署阶段的高频报错端口占用、跨域与数据库连接失败部署时第一类是端口问题。Spring Boot 默认使用 8080如果你本机的 8080 被其他应用占用常见的是本地调试的另一个 Jav 进程启动日志里会出现Port 8080 was already in use。切到 8081 或 9090 都能解但最省事的做法是用-Dserver.port0随机分配端口再在日志里查实际端口。第二类是跨域问题。前后端分离项目前端跑在 5173Vite 默认后端跑在 8080浏览器会拦截跨域请求。常见的处理方式是加一个配置类允许指定源访问Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }第三类是 MySQL 连接失败。最容易踩的坑是 MySQL 8 以上版本默认用caching_sha2_password认证插件旧版 JDBC 驱动不支持。解决办法是确保pom.xml里的驱动版本在8.0.11以上。第四类是时区导致的时间漂移如果application.yml的serverTimezone不写可能让你计费模块的「白天/夜间」判断完全错乱。5. 进阶技巧用一张表实现高峰时段的自助化并发验证系统做完后判断它能不能真正扛住一个中型停车场高峰时段的吞吐量可以做一次并发验证。常见的做法是写一个简单的多线程模拟器循环调用入场接口观察是否有「超卖」现象。public class EntryConcurrencyTest { public static void main(String[] args) throws InterruptedException { int threadCount 30; CountDownLatch latch new CountDownLatch(threadCount); ExecutorService executor Executors.newFixedThreadPool(threadCount); AtomicInteger successCount new AtomicInteger(0); AtomicInteger failCount new AtomicInteger(0); for (int i 0; i threadCount; i) { String plateNo String.format(模拟车牌%03d, i); executor.submit(() - { try { // 调用入场接口 boolean success ParkingApiClient.entry(plateNo); if (success) { successCount.incrementAndGet(); } else { failCount.incrementAndGet(); } } finally { latch.countDown(); } }); } latch.await(30, TimeUnit.SECONDS); System.out.println(成功入场: successCount.get() , 失败: failCount.get()); executor.shutdown(); } }要做这个验证需要在库里预先准备 30 条车位数据。预期结果是失败次数为 0也就是 30 辆车全部拿到空闲车位且不冲突且并发丢单。如果你在日志里看到Deadlock found或Failed to obtain JDBC Connection根源通常是「锁范围过大 事务时间过长」的组合问题。指令是淘汰掉一秒内多辆车全查同一张表所以还要把车牌的幂等校验索引建对。顺着这个思路把停车场的「入场检查车牌是否在场」逻辑改造成用数据库唯一索引来兜底给t_parking_order添加一个「车牌 状态」的条件唯一索引或者在业务代码中用SELECT ... FOR UPDATE锁住车牌维度再做判断。两者选其一即可完整防住同一辆车的重复入场。这里推荐用行锁形式并配合事务因为它的思路最贴近设备识别场景下的真实需求。本文还有配套的精品资源点击获取
返回列表