ARTICLE DETAIL

资讯详情

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

Spring Boot+Vue实现Web停车场管理系统:数据库设计到部署全解析

Spring Boot+Vue实现Web停车场管理系统:数据库设计到部署全解析 简介一份基于Web停车场管理系统的设计与实现毕业设计文档面向计算机相关专业的学生、开发者以及需要搭建类似管理系统的技术人员。文档围绕日益突出的停车难问题给出了完整的B/S架构解决方案采用J2EE标准、Tomcat 8.0服务器、Eclipse 4.6开发环境与MySQL 5.5.37数据库并基于MVC设计模式分层实现。内容覆盖绪论、系统需求分析、系统设计、系统实现、系统测试与性能评估等关键章节分别阐述了车位管理、收费管理、停车场数据管理、用户信息管理等功能模块的设计思路与实现方法可帮助读者梳理从需求到编码的完整流程也可作为毕业设计、课程设计或工程项目的参考模板。压缩包内为单个docx格式的完整设计文档大小2.88MB便于按章节阅读、检索和二次编辑。试运行结果表明系统具有良好的性能与扩展性可满足中大型停车场日常管理需求。目前已有475人学习使用适合需要快速掌握Web管理系统架构设计的中级学习者。1. 基于Web的停车场管理系统到底在解决什么和朋友聊到停车场管理系统的信息化改造时他跟我吐槽过一句话车位到底还有几个只有门口保安知道今晚收益多少要等第二天手工对账。这是基于 Web 的停车场管理系统最想解决的两个痛点——车位信息不透明、账目追溯靠纸质单据。这套系统的核心就是把停车场的日常流程搬进浏览器管理员看实时车位图收费员扫码录入车牌车主在手机上查空位订单和报表自动汇总。适合两类人看这篇文章一类是正在做毕业设计或课程设计需要一个完整可演示的 web 项目另一类是手里有小型停车场资源想低成本做信息化改造。下面从选型理由、数据库设计、核心代码、踩坑记录到部署验证完整走一遍方案来自我做过和帮人排查过的多个同类系统照着改比从零开始省力得多。2. 技术选型与系统架构为什么这套停车场管理系统选 Spring Boot Vue 而不是 JSP2.1 选型逻辑资料多、能搜到答案的技术才值得选停车场业务的逻辑并不复杂复杂的地方在规则和并发。入车、出车、查询三个核心动作就能概括全部业务流程。真正决定一个项目后续能不能维护下去的因素是你遇到报错时能不能在搜索引擎里快速找到同类问题。Spring Boot 作为 java web 生态里使用最广泛的后端框架从 IDEA 新建工程到打成 jar 部署几乎每一个报错都有前人留下的回答Vue.js 在 web 前端开发里同样如此组件化写法天生适合做“每个车位一个格子”这种界面数据库选 MySQL无论本机开发还是云服务器部署相关经验都足够多。对毕业设计和中小型停车场信息化改造来说容错性比先进性重要得多。老方案里常见的是 JSP Servlet代码全写在后端前端同事改一个样式要等后端重启 Tomcat效率很低。现在的主流做法是前后端分离后端只出接口前端单独跑工程两边各自开发、联调时对接口文档。如果目标只是快速交一个能演示的 demo老方案也能做但凡是后续还有迭代、换皮、加功能JSP 那条路十有八九要返工这是我的血泪经验。企业级 web 开发里最常见的组合就是 Spring Boot Vue MySQL学这一套的投入产出比是最高的也是招聘市场上最好交代的。2.2 系统模块划分与一次入场请求的完整链路我习惯把系统拆成五个模块车位管理、车辆管理、收费管理、订单统计、系统管理。车位管理管车位的增删改和状态开关车辆管理管车牌登记和月卡绑定收费管理是核心模块包含入场登记、出场结算、优惠减免订单统计做按天、按月、按区域的汇总系统管理管用户和角色。三个客户端共用一套后端接口管理端跑在 PC 浏览器上看报表和配置收费端跑在岗亭的平板或电脑浏览器上界面简洁、按钮大、录入快车主端是手机浏览器页面查空位、查历史订单、自助缴费。这样比给每个端单独写一套后端省一半工作量也不会出现各端数据口径不一致的问题。后端遵循经典的三层结构Controller 只收参数、只回结果Service 层处理业务和计费Mapper 层访问数据库。一次完整入场请求的链路是这样的收费员在岗亭页面输入车牌点击“入场”按钮浏览器 POST 请求到 /api/parking/entryController 校验参数后调 EntryServiceService 先查该车牌是否已在场再从 parking_space 表选一个空闲车位用乐观锁占用车位并生成一条 parking_order 记录最后把车位编号返回前端车位图对应格子变成红色。整条链路响应目标设在 200 毫秒以内超过这个数就先查 SQL 有没有走索引别急着加缓存。接口设计阶段就要约定返回结构否则三个端联调时各写各的解析逻辑后期改接口会牵连一堆页面。我用的统一结构是 { code: 0, message: ok, data: {...} }code 为 0 表示成功非 0 表示业务失败HTTP 状态码只保留 200、401、403、500 这几个语义。这个约定看起来不起眼但能避免前端拦截器里写一堆分支去判断哪种情况算成功也能让新加入的前端同事一眼就懂。2.3 工程结构与开发环境从 IDEA 创建到前后端联调跑通最常见的做法是后端用 IDEA 创建 Spring Boot 工程前端用 Vue CLI 或 Vite 单独建工程两个工程放在同一个父目录下。后端选 Spring Web、MyBatis、MySQL Driver 三个起步依赖前端默认模板就够。IDEA 2024 版本创建 web 项目的向导比老版本顺滑很多依赖选完直接生成不用手动下载 jar 包。工程结构我习惯这样摆backend/ ├── pom.xml ├── src/main/java/com/example/parking │ ├── controller/ │ ├── service/ │ ├── mapper/ │ ├── entity/ │ └── config/ └── src/main/resources ├── application.yml └── mapper/*.xml后端 controller、service、mapper、entity、config 五个包配置和 MyBatis 的 XML 放 resources。前端的 src 下只留 views、api、components、router 四个目录。接口前缀统一 /api联调时前端开发服务器走代理把 /api 转发到后端 8080 端口。第一次跑通的标准是后端启动后浏览器直接访问 http://localhost:8080/api/parking/spaces 能返回车位 JSON前端启动后页面能通过代理取到同一份 JSON。这样前后端通路就建立了后面再加功能只是在这个结构里不断填内容不会越改越乱。3. 停车场管理系统的数据库设计从车位表到订单表的 DDL 与索引取舍3.1 四张核心表业务关系怎么收敛停车场业务建模我见过两种极端。一种是把表拆得极细停车区域、收费规则、优惠券类型、操作日志全部独立成表查一个简单报表要 join 五张表另一种是图省事一张大表存所有字段日积月累数据多了以后索引和统计都慢。两种都踩过坑后来收敛成四张核心表加一张配置表parking_space 车位表、vehicle 车辆表、parking_order 订单表、sys_user 用户表再加上 parking_config 计费配置表。为什么订单表是整个系统的核心因为停车场一切经济活动最终都落在订单上入场时生成一条状态为“进行中”的订单出场时补上出场时间、时长和金额状态变成“已完结”。车位表存储每个车位的位置和当前状态车辆表存储固定车辆信息用户表管登录和权限它们和订单表的关系都是一对多。计费配置表单独抽出来是我吃过亏之后的决定——之前把费率写死在代码里活动期间改一次价格要重新部署后来都改成配置表了管理员在后台改数字就能生效。3.2 建表 SQL字段类型、默认值与唯一约束这套 DDL 在 MySQL 8 下直接执行能跑通。字符集用 utf8mb4目的是让车牌中的生僻字和省市区缩写不出乱码。表结构里藏着三个细节车牌号字段建唯一索引、金额字段用 DECIMAL(10,2)、车位表预留 version 字段。这三处都是后面防止出大问题的关键。CREATE DATABASE IF NOT EXISTS parking_system DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci; USE parking_system; CREATE TABLE parking_space ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, space_no VARCHAR(20) NOT NULL COMMENT 车位编号如 A-001, area VARCHAR(20) NOT NULL DEFAULT A区 COMMENT 区域, status TINYINT NOT NULL DEFAULT 0 COMMENT 0空闲 1占用 2预留 3故障, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, UNIQUE KEY uk_space_no (space_no) ) ENGINEInnoDB COMMENT 车位表; CREATE TABLE vehicle ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, plate_no VARCHAR(20) NOT NULL COMMENT 车牌号统一大写, owner_name VARCHAR(50) DEFAULT NULL COMMENT 车主姓名, phone VARCHAR(20) DEFAULT NULL COMMENT 联系电话, vehicle_type TINYINT NOT NULL DEFAULT 0 COMMENT 0临时 1月租 2内部, register_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_plate_no (plate_no) ) ENGINEInnoDB COMMENT 车辆表; CREATE TABLE parking_order ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL COMMENT 业务订单号, plate_no VARCHAR(20) NOT NULL COMMENT 车牌号, space_no VARCHAR(20) NOT NULL COMMENT 车位编号, entry_time DATETIME NOT NULL COMMENT 入场时间, exit_time DATETIME DEFAULT NULL COMMENT 出场时间, duration_minutes INT NOT NULL DEFAULT 0 COMMENT 停车时长分钟, amount DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 应收金额元, status TINYINT NOT NULL DEFAULT 0 COMMENT 0进行中 1已完结 2异常, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), KEY idx_plate_time (plate_no, entry_time), KEY idx_exit_time (exit_time) ) ENGINEInnoDB COMMENT 停车订单表; CREATE TABLE sys_user ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT 密码哈希值, real_name VARCHAR(50) DEFAULT NULL, role TINYINT NOT NULL DEFAULT 1 COMMENT 1管理员 2收费员, status TINYINT NOT NULL DEFAULT 1, UNIQUE KEY uk_username (username) ) ENGINEInnoDB COMMENT 系统用户表;四点参数说明。第一parking_space 的 status 用 TINYINT 存数字而不是字符串前端展示时再做映射避免中文乱码和大小写不一致。第二vehicle 表只记录固定车辆临时车不强制登记因为离场后能不能找到车主不重要重要的是订单里有没有车牌。第三order_no 用业务单号而不是主键自增好处是不暴露订单量也方便错误排查时按单号对账。第四金额字段用 DECIMAL(10,2)绝对不用 float 或 double——停车费看着只有几块钱但累计报表一旦精度丢失月底对账会让你想摔键盘。3.3 初始化数据与报表查询的索引验证建完表先灌一批初始数据。车位至少 20 个覆盖 A/B/C 三个区域用户至少两个一个管理员一个收费员订单要同时包含进行中和已完结的日期要跨越昨天和今天。这样前后端联调时报表接口一出来就能看到分组聚合效果不会因为数据太少看不出口径问题。INSERT INTO parking_space (space_no, area) VALUES (A-001, A区), (A-002, A区), (A-003, A区), (B-001, B区), (B-002, B区), (C-001, C区); INSERT INTO sys_user (username, password, real_name, role) VALUES (admin, 加密后的密码串, 系统管理员, 1), (cashier1, 加密后的密码串, 岗亭收费员, 2);数据到位之后用 EXPLAIN 验证一下索引是否生效EXPLAIN SELECT * FROM parking_order WHERE plate_no 京A12345 AND entry_time 2025-06-01 ORDER BY entry_time DESC;如果 type 列出现 ALL说明在走全表扫描需要检查联合索引是否建上如果是 ref 或 range 就说明索引正常工作。这个习惯花不了半分钟能提前发现 80% 以上的慢查询问题。索引不是越多越好写入记录时也要同步维护索引停车场这种写多读也多的场景索引数量控制在表数量的一到两倍以内比较合理。4. 核心业务代码计费规则、车位状态流转与前端实时交互4.1 计费引擎先算分钟再换金额避免规则改到哭计费规则是这套系统里最容易藏 bug 的地方。最常见的规则是“前 30 分钟免费超过 30 分钟后按每小时 5 元不足 1 小时按 1 小时计单日封顶 30 元”。这个规则翻译成代码关键不在乘法而在时间换算。直接拿毫秒差除以 3600000 得到小时数小数部分怎么处理就成了问题——丢弃会让金额偏小四舍五入会让规则解释不通。正确的步骤是先算总分钟再扣免费时长剩下的分钟数向上取整成小时最后乘单价超过封顶就取封顶值。public BigDecimal calculateFee(LocalDateTime entryTime, LocalDateTime exitTime) { if (entryTime null || exitTime null || exitTime.isBefore(entryTime)) { throw new IllegalArgumentException(入场时间和出场时间非法); } long minutes Duration.between(entryTime, exitTime).toMinutes(); if (minutes 0) { minutes 1; } if (minutes FREE_MINUTES) { return BigDecimal.ZERO; } long billableMinutes minutes - FREE_MINUTES; long hours (billableMinutes 59) / 60; BigDecimal rawAmount new BigDecimal(hours).multiply(HOURLY_RATE); if (rawAmount.compareTo(DAILY_CAP) 0) { rawAmount DAILY_CAP; } return rawAmount.setScale(2, RoundingMode.HALF_UP); }三段逻辑分别说明。第一段校验入场时间不能晚于出场时间这属于基本边界防护没有的话后端会炸出奇怪的 NPE。第二段把总分钟数减掉免费分钟数然后 (billableMinutes 59) / 60 是向上取整的标准写法31 分钟会进位到 1 小时。第三段用 compareTo 和单日封顶比大小然后 setScale 保留两位小数。FREE_MINUTES、HOURLY_RATE、DAILY_CAP 这三个参数我建议从配置表读取不要写死成常量——之前接过一次运维工单停车场国庆搞活动临时改免费时长改代码再重新部署花了两个小时期间所有出场订单计费都错了。跨自然日订单是计费的另一个难点。前一天 23:50 入场、当天 00:20 出场停在“零点”两侧直接按总时长算可能突破单日封顶。处理办法是把订单按自然日切成两段每一段在当天内独立计费、独立封顶最后累加。实现上需要把入场到出场之间的每一天都枚举出来分段计算。写完之后一定要用这种跨日用例去测不能只看普通时段的用例。4.2 并发占用车位乐观锁与 Redis 锁的取舍两个收费员在同一秒对同一个车位做入场操作这是停车场系统并发问题的典型场景。代码顺序是“先查空闲车位、再更新状态为占用”两次操作之间有间隙并发时两条线程可能读到同一个空闲车位。解决思路有两种数据库乐观锁和 Redis 分布式锁。乐观锁的做法是在车位表加 version 字段更新时带上拿到的版本号int rows spaceMapper.updateStatusByIdAndVersion(spaceId, 1, version); if (rows 0) { throw new BusinessException(该车位刚被占用请重新选择); }对应的 MyBatis 更新语句是UPDATE parking_space SET status 1, version version 1 WHERE id #{spaceId} AND version #{version}rows 返回 0说明更新时版本号已经不匹配这个车位在两步之间被别人抢走业务层直接抛异常前端提示收费员换一个车位。这个方案不需要引入额外中间件几十个车位的场景完全够用缺点是在并发很高时请求会以失败结束需要前端提示收费员重新选择而不是转圈白屏。如果车位数到达几百个、高峰期同时入场请求量明显上升我会换 Redis 分布式锁。锁的 key 用车位编号例如 lock:space:A-001通过 SETNX 抢锁抢到锁的线程才执行选位和建单执行完释放。这里有一个必须提醒的点锁一定要设置过期时间比如 5 秒否则线程崩溃后锁永远不释放这个车位就废了。引入 Redis 意味着生产环境多一个要维护的组件我的态度很明确真实并发没到那个量级就先别上分布式锁乐观锁在毕业设计和中小型系统里足够重点是把“冲突之后如何反馈给用户”这条交互链路做完整。4.3 前端车位图与订单列表Vue 组件加轮询前端的工作集中在两块实时刷新车位状态、处理入出场操作。车位图用网格组件实现每个格子显示车位号背景色映射状态绿色空闲、红色占用、灰色故障。后端提供 /api/parking/spaces 接口前端每 5 秒轮询一次刷新。为什么不用 WebSocket因为停车场状态变化的实时性要求不高误差 5 秒在现场完全可以接受轮询的实现和维护成本低一个量级。const spaces ref([]); async function loadSpaces() { const res await api.get(/parking/spaces); spaces.value res.data; } setInterval(loadSpaces, 5000); loadSpaces();模板里用 v-for 渲染格子状态色用一个方法映射div classspace-grid div v-forspace in spaces :keyspace.id :class[space-cell, stateClass(space.status)] {{ space.spaceNo }} /div /div这里三个经验值得记下来。第一轮询间隔不要低于 3 秒停车场高峰期几十个岗亭同时打开页面每个页面 5 秒拉一次后端压力已经不小。第二状态映射写成独立方法 class后续加“预留”状态或“故障”状态时只改一处不要在每个格子组件里复制粘贴。第三api 实例统一封装 axiosbaseURL 指到 /api开发环境靠代理转发生产环境交给 Nginx 转发前端代码里从此不写一个带端口的地址。出场流程是收费员输入车牌前端调 /api/parking/exit/query 拿到时长和金额展示出来收费员向车主收款后再调结算接口完成出场车位图刷新。注意查询和结算必须是两个接口因为收费员需要先看到金额再操作前端拿到金额后还要二次确认避免误操作。5. 避坑记录停车场 Web 系统最常见的 5 个翻车现场5.1 计费时间计算出错现场金额和后台对不上现象出场“1 小时 1 分”的车收费员算出来 5 元系统显示 10 元同一辆车停 31 分钟和停 59 分钟收一样的钱车主当场投诉。原因后端用了毫秒差直接除 3600000 再取整或者用了 double 做除法又做了四舍五入两次误差叠加后规则完全失真。解决统一用 Duration.between 算总分钟再按“免费时长 → 剩余分钟 → 向上取整成小时 → 乘单价 → 比封顶”的顺序计算。测试用例必须包含 29 分钟、30 分钟、31 分钟、60 分钟和跨日这五个边界值可以做成一张边界值表贴在代码注释里方便后来改规则的人对照。5.2 同一个车位被同时分配给两辆车现象车场高峰期两个岗亭先后对同一个车位做了入场操作系统把同一个车位号返回给两辆不同的车现场秩序混乱。原因代码是“先查空闲车位、再更新状态为占用”的顺序操作两条并发线程之间有空隙都读到了“空闲”的结果。解决用乐观锁版本号更新车位状态更新受影响行数为 0 时抛业务异常提示重新选位如果项目已经引入 Redis就用 SETNX 加锁并设置过期时间。验证方法用 JMeter 并发 50 个线程同时请求入场接口跑完之后统计 status1 的车位是否有重复订单。这个测试是上线前必须做的一项不做就是在赌现场不会并发。5.3 金额精度丢失报表里出现 4.949999现象单笔订单金额出现 4.949999 这种数月度合计总差几毛钱财务对账怎么都平不了。原因后端用 float 或 double 存金额数据库表字段也是 FLOAT。二进制浮点数在运算过程中无法精确保存十进制小数这是计算机原理决定的不是代码写错但结果是错的。解决Java 侧金额全部用 BigDecimal数据库字段改成 DECIMAL(10,2)前端展示前再转成字符串。顺手做一次全局排查搜索代码里所有 double 和 float 变量凡是用在金额上的全部换掉。停车费按元到分就够不需要更小的小数位。5.4 车牌号格式不统一同一辆车查出两条档案现象同一辆临牌车第二次入场时查不到历史记录车主端订单列表缺一半数据导出的报表里“京A12345”和“京a·12345”被当成不同的车。原因录入端没有做统一格式化用户怎么输入就怎么存数据库字段也没有唯一约束脏数据越积越多。解决后端在写入前统一做归一化——去首尾空格、小写字母转大写、去掉车牌中间的点号和横杠。vehicle 表的 plate_no 字段建唯一索引兜底。停车订单表同样在写入时做归一化否则历史数据仍旧对不上。这个改造最好在上线前做完上线后再补数据清洗非常痛苦。5.5 跨时段计费规则失效过夜车出场金额离谱现象前一天晚上入场、第二天早上出场的车金额要么高得离谱要么低得异常单日封顶完全没有生效。原因计费引擎只按总时长算了一次金额没有按自然日分段单日封顶逻辑自然也失效了。解决计费引擎按自然日把订单拆分成多段每一天独立计算当天的费用最后累加。配一张计费配置表把免费分钟数、每小时单价、单日封顶做成三个字段规则调整时改配置表而不是改代码。写完这段逻辑后第一个测试用例必须是“前一天 23:50 入场、当天 00:20 出场”这个用例能同时暴露分段、取整、封顶三个问题。6. 部署与验证从开发机到服务器的最后一公里6.1 打包与部署前后端分离的标准发布流程后端用 Maven 打包前端构建成静态文件Nginx 托管并反向代理 /api 前缀。流程和命令mvn clean package -DskipTests npm run build后端 jar 包放到服务器用 systemd 托管前端 dist 放到 Nginx 站点目录。Vue Router 用了 history 模式时Nginx 必须加 try_files否则直接访问子路由会 404。一套最小可用的 Nginx 配置location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; }部署完开浏览器开发者工具看一眼/api 请求标红就优先查代理配置和防火墙页面白屏就查资源是 404 还是接口超时。上线前把后端的 Swagger 文档关掉或加访问权限这是 web 服务器安全里最容易被忽略的一步等于告诉别人你的接口长什么样。6.2 验证清单上线前必须跑通的四类测试我每次上线前按四类走一遍功能链路用真实车牌从入场到出场核对金额并发测试用 JMeter 打 50 并发到入场接口确认没有车位重复分配边界测试把时间设在午夜和月初验证跨日计费权限测试用收费员账号访问管理员接口确认返回 403。测试类别重点用例通过标准功能链路临牌入场→出场→结算金额与人工计算一致并发测试50 并发同时抢车位无重复分配边界测试跨自然日计费封顶规则正确权限测试收费员访问管理接口返回 403这四类验证做完系统才敢放给现场用。不要跳过并发测试停车场系统第一天翻车多半就是并发问题。6.3 进阶缓存、异步与监控三件套跑顺之后值得做三件事车位状态缓存到 Redis查询接口少吃数据库出场结算异步化主流程只算钱、返回结果报表统计和通知推送丢到后台线程池执行再给接口加耗时日志定期捞超过 500 毫秒的慢请求。这三件不是第一版必须做的但做了之后的维护体验完全是两个级别。最后说一个我自己的习惯每次改完计费相关代码不先看测试报告而是亲手用一条真实车牌从入场走到出场再对一遍 Excel 手算金额。这个习惯救过我很多次因为计费规则里的“半小时”“一小时”“封顶”写代码的人和收费员的理解经常不一样只有真实数据能暴露这种偏差。希望这套方案和这些踩坑记录能帮你在做自己的停车场管理系统时少走弯路。本文还有配套的精品资源点击获取
返回列表