简介面向计算机相关专业学习者与毕业设计开发者提供一份基于Web的停车场管理系统完整设计与实现文档围绕城市化背景下的停车难问题从课题背景、目的意义、国内外现状到开发环境与设计方法均有系统论述。文档明确采用Java、Spring Boot构建后端前端结合HTML5、CSS3、JavaScript及Bootstrap/Vue.js提升交互数据库采用MySQL并涉及RESTful API、AJAX、JWT、GIS等关键技术可作为理解Web系统开发流程与撰写毕业设计论文的参考范例。内容预览显示正文包含绪论、系统分析、系统设计与实现、数据库设计等章节功能模块覆盖用户管理、车位管理、计费管理、预约管理等有助于快速把握整体架构与核心业务逻辑。资源共1个docx文件压缩包大小约947KB已有134人学习适合用于课程设计、毕业设计参考及停车场管理项目的前期方案调研。1. 为什么先把 web 停车场管理系统拆成「数据、接口、页面」把停车场从人工岗亭搬到网页上表面看是加了一块余位大屏和扫码付费页面实际要解决的是“一辆车从进场到出场这十几分钟内数据怎么不重、不丢、不错”。web 停车场管理系统听起来是前端工程但立项后最先暴露问题的地方往往是数据模型和计费规则入场没登记、出场重复扣费、车位状态和订单对不上最终都表现为页面上的怪象。动手做之前先按“数据闭环”而不是“页面清单”来拆设计入场流水记录车辆和车位占用状态决定余位数字结算订单承载费用报表再从中聚合。页面只负责把这三段数据读出来并写回去。系统设计阶段的产出不是原型图而是“谁在什么时间改动哪张表、由哪个接口触发”的状态流转图。这套拆法适合三类人做课程设计和毕设的学生给物业替换 Excel 台账的团队以及想把旧管理系统迁移到浏览器端的开发者。确定技术栈之前先回答三个问题入场是否允许重复刷卡、离场费用由谁确认、页面多久要看到余位变化。答案会直接决定建表方式和接口写法。2. 停车场管理系统的数据库模型车位、流水与计费规则怎么建表2.1 一套数据闭环入场流水、占用状态、结算订单各管一段许多半成品系统把车位占用状态放在前端内存里页面一刷新就回到初始值原因就是没有把状态落到数据库。更稳妥的做法是让四张表各管一段car_space保存车位的当前占用状态parking_record保存每一次进出流水parking_order保存结算结果fee_rule保存可修改的计费参数。这里的核心原则是「状态由事件推导不能只有状态本身」car_space.status只是一个便于查询的冗余字段真正的在停事实是parking_record中存在exit_time IS NULL的记录。入场动作把流水写进去并更新车位占用离场再把同一号牌的在停记录补上离场时间同时生成订单。三步分别落进两个事务任何一个环节断掉都能从表数据反查出来。报表统计也不直接依赖车位状态而是按月扫描parking_order这样可以避免“余位数字显示正常但收费总额对不上账”的尴尬。2.2 核心字段怎么定金额用十进制、并发用版本号字段设计上值得提前避开的坑有三类金额用浮点数累计产生误差订单号依赖自增主键导致重复提交无法识别两台闸机同时抬杆抢同一个车位。下面这张表是落地版本中建议保留的关键字段。表关键字段能解决什么问题car_spacecode、status、version、update_time用条件更新抢占车位避免并发分配同一车位parking_recordplate_no、entry_time、exit_time、space_id、order_id用exit_time IS NULL判断在停车辆链路可回溯parking_orderorder_no、record_id、amount、paid_amount、statusorder_no做幂等键防止重复扣费record_id关联流水fee_rulerule_type、start_time、end_time、priority计费规则可配置改价不用改代码金额用DECIMAL(10,2)所有时间用DATETIME如果你要跨时区部署再加一个zone_offset。version是数值型初始为 0每次更新加 1。停车场系统不像电商那样高并发但岗亭设备的重试机制很容易造成同一号牌被同时写入order_no的唯一索引就是最后一道防线。2.3 建表 SQL 与在停车辆查询按 MySQL 8 语法下面这一个最小集合可以直接跑通正式环境再加索引和额外字段。CREATE TABLE car_space ( id BIGINT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(20) NOT NULL COMMENT 车位编号, status TINYINT NOT NULL DEFAULT 0 COMMENT 0 空闲/1 占用/2 禁用, version INT NOT NULL DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_code (code) ); CREATE TABLE parking_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, plate_no VARCHAR(20) NOT NULL COMMENT 车牌号, space_id BIGINT NOT NULL, entry_time DATETIME NOT NULL, exit_time DATETIME NULL, order_id BIGINT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_plate_exit (plate_no, exit_time) ); CREATE TABLE parking_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, record_id BIGINT NOT NULL, plate_no VARCHAR(20) NOT NULL, amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, paid_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, status TINYINT NOT NULL DEFAULT 0 COMMENT 0 待支付/1 已支付/2 已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME NULL, UNIQUE KEY uk_order_no (order_no) ); CREATE TABLE fee_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, rule_type VARCHAR(20) NOT NULL COMMENT free/unit/step/night, start_time TIME NULL, end_time TIME NULL, first_minutes INT NOT NULL DEFAULT 0, first_fee DECIMAL(10,2) NOT NULL DEFAULT 0.00, cycle_minutes INT NOT NULL DEFAULT 0, unit_fee DECIMAL(10,2) NOT NULL DEFAULT 0.00, cap_per_day DECIMAL(10,2) NULL, priority INT NOT NULL DEFAULT 0 );parking_record的联合索引(plate_no, exit_time)是为了支撑“查某号牌最近一条在停记录”和“查所有未离场车辆”两个高频动作。日常余位列表直接读car_space但页面上的在停信息要回到流水表取entry_time避免把车位状态表当成流水账来用。常用查询如下SELECT r.plate_no, s.code AS space_code, r.entry_time, TIMESTAMPDIFF(MINUTE, r.entry_time, NOW()) AS stay_minutes FROM parking_record r JOIN car_space s ON r.space_id s.id WHERE r.exit_time IS NULL AND r.order_id IS NULL ORDER BY r.entry_time DESC;order_id IS NULL表示这条流水还没有形成结算订单也就是“待支付”的在停车辆。如果这里过滤条件写错列表会混入已结算订单前端支付按钮就会点在已完成的单子上。提示入场和离场都依赖plate_no要约定统一的大写格式数据库查询时用UPPER(TRIM(plate_no))避免“京A12345”和“京a12345”被当成两辆车。3. web 停车场管理系统的后端计费接口入场登记与离场结算3.1 入场接口先查在停、再分配车位、后写流水后端接口先定边界按下面这张表收敛不要随手加字段。方法路径用途需要参数返回POST/api/v1/entry入场登记plateNo、spaceCoderecordId、entryTimePOST/api/v1/exit离场结算plateNo、orderNoorderId、amountGET/api/v1/spaces车位实时状态无车位列表与余位GET/api/v1/orders订单列表status、page、size分页订单入场接口的代码量很小但顺序不能乱PostMapping(/api/v1/entry) public ResultEntryVO entry(RequestBody EntryRequest req) { // 1. 幂等同一号牌不允许同时存在两条在停记录 if (parkingRecordDao.existsRunning(req.getPlateNo())) { return Result.conflict(该车辆已在场内); } // 2. 用条件更新抢占车位返回 0 说明车位已变 int updated spaceDao.occupy(req.getSpaceCode()); if (updated 0) { return Result.conflict(目标车位不可用请刷新余位); } // 3. 写流水服务端时间统一作为 entryTime ParkingRecord record new ParkingRecord(); record.setPlateNo(req.getPlateNo().toUpperCase()); record.setSpaceId(spaceDao.findIdByCode(req.getSpaceCode())); record.setEntryTime(LocalDateTime.now()); parkingRecordDao.insert(record); return Result.ok(EntryVO.of(record)); }这里有两个容易漏的细节spaceDao.occupy执行的是UPDATE car_space SET status1, versionversion1 WHERE code? AND status0影响行数为 0 就不能继续existsRunning和occupy之间不是原子操作真正兜底的是数据库唯一约束或在停记录上的唯一索引。管理端还要加登录校验建议用成熟的 Spring Security 方案管理接口全部要求 token不要为了省事把服务暴露成任何人可以调用。3.2 计费计算的两种落地规则表优先与分钟向上取整计费常见需求是前 30 分钟免费超过后首小时 5 元之后每 30 分钟 2 元夜间时段封顶 20 元。常见的做法是规则进fee_rule表让运营自己改而不是写成一长串 if-else。规则表里的priority字段用来解决时段交叉比如“日间计时”和“夜间封顶”两条规则都命中的时候取priority值小的那一条。public FeeResult calculate(LocalDateTime entry, LocalDateTime exit, boolean member) { if (member) { return FeeResult.free(月租车); } long minutes Duration.between(entry, exit).toMinutes(); if (minutes 0) minutes 1; ListFeeRule rules feeRuleDao.findEnabledOrderByPriority(); for (FeeRule rule : rules) { if (!rule.cover(entry, exit)) continue; // 时段不覆盖则跳过 if (minutes rule.getFirstMinutes()) { return FeeResult.of(rule.getFirstFee(), rule.getName()); } long extraMinutes minutes - rule.getFirstMinutes(); long cycles (extraMinutes rule.getCycleMinutes() - 1) / rule.getCycleMinutes(); BigDecimal fee rule.getFirstFee() .add(rule.getUnitFee().multiply(BigDecimal.valueOf(cycles))); if (rule.getCapPerDay() ! null fee.compareTo(rule.getCapPerDay()) 0) { fee rule.getCapPerDay(); } return FeeResult.of(fee, rule.getName()); } return FeeResult.of(BigDecimal.ZERO, 缺失规则); }(extraMinutes cycleMinutes - 1) / cycleMinutes是“向上取整”的标准写法。比如超出 31 分钟、周期 30 分钟计算结果就是 2 个计费周期。这里的坑在于免费时段如果入场时间在免费时段末尾出场跨越免费边界应该按“首段免费分钟数已消耗完剩余按正常周期计”处理而不是直接不收费。Duration.between按绝对分钟数计算跨天时不受影响如果用到LocalTime则要小心跨 24 点的减法。注意收费结果必须由后端统一计算前端只能展示。扫码支付的金额回调也要以服务端落库的amount为准防止人为篡改请求参数。3.3 离场结算幂等键、事务边界与状态流转离场是入口操作和财务操作的交汇点要做成“可以安全重试”的接口。客户端每次调用都带上自己生成的orderNo服务端收到重复orderNo直接返回已有订单不产生新的扣费记录PostMapping(/api/v1/exit) Transactional(rollbackFor Exception.class) public ResultExitVO exit(RequestBody ExitRequest req) { if (orderDao.existsByOrderNo(req.getOrderNo())) { return Result.ok(orderDao.getByOrderNo(req.getOrderNo())); // 幂等返回 } ParkingRecord record parkingRecordDao.lockRunningByPlateNo(req.getPlateNo()); if (record null) { return Result.conflict(未找到在停记录); } FeeResult fee calculate(record.getEntryTime(), LocalDateTime.now(), req.getMember()); ParkingOrder order ParkingOrder.create(req.getOrderNo(), record, fee); orderDao.insert(order); parkingRecordDao.finish(record.getId(), order.getId()); spaceDao.release(record.getSpaceId()); return Result.ok(ExitVO.of(order)); }lockRunningByPlateNo使用SELECT ... FOR UPDATE锁住这条在停流水避免两个客户端同时离场把车位释放两次。spaceDao.release也要写成条件更新UPDATE car_space SET status0, versionversion1 WHERE id? AND status1防止把已经禁用的车位重新置为空闲。订单状态建议固定为0 待支付 - 1 已支付 - 2 已取消现金结算可以一步到位更新为已支付在线支付则等回调再改状态。不要直接删除订单停车场对账需要保留所有历史状态。4. 停车场管理系统的前端页面与状态刷新车位面板如何自动同步4.1 组件拆分状态列表、车位地图和计费表单互不阻塞web 项目不宜做成一整个页面塞全部功能。按数据来源拆分组件管理端可以拆成四个车位地图SpaceMap.vue显示二维停车位占位订单列表OrderTable.vue展示在停和结算计费规则FeeRuleForm.vue提供表单修改数据统计StatPanel.vue负责今日收入、车流量。组件之间不要直接修改对方的数据统一通过一个 store 保存spaces、runningOrders、todayIncome。import { ref, onMounted, onUnmounted } from vue const spaces ref([]) const runningOrders ref([]) let timer null async function refresh() { const [spaceRes, orderRes] await Promise.all([ fetch(/api/v1/spaces), fetch(/api/v1/orders?statusrunning), ]) spaces.value await spaceRes.json() runningOrders.value await orderRes.json() } function startPolling(intervalMs 30000) { refresh() timer setInterval(refresh, intervalMs) } function stopPolling() { if (timer) { clearInterval(timer) } } onMounted(() startPolling(30000)) onUnmounted(stopPolling)轮询间隔设成 30 秒比较平衡但要注意refresh里的两个请求是并发发出的不能保证同时返回。更稳妥的做法是用Promise.all后整体赋值这样页面不会出现“余位已经更新、订单还是旧数据”的不一致状态如果请求耗时会超过轮询周期还要加一个isRefreshing标志避免上一个请求还没结束又发起下一次。4.2 三十秒轮询还是 WebSocket先看数据规模和运维预算选择哪种方式要看吞吐量和页面呈现。如果只是管理端看余位和订单30 秒轮询足够如果要做现场大屏希望抬杆瞬间余位就变化则适合加 WebSocket。一个可用的判断基准是管理后台用轮询大屏页面用 WebSocket两端通过同一个状态 store 连接。对比项轮询WebSocket服务端改动无需要维护长连接和推送通道数据实时性最差延迟一个轮询周期秒级断线恢复简单重连即可需要心跳、重连和补拉差量适合页面后台列表、报表余位大屏、车位地图用 WebSocket 时后端推给前端的消息只放变化前端收到后刷新对应的space对象再触发一次订单列表刷新。不要每次推送整个车位数组停车场几百个车位量不大但在弱网环境里大消息体很容易造成阻塞。消息格式建议固定为{ type: SPACE_OCCUPIED, data: { spaceCode, recordId } }前端按type分发而不是在回调里写一长串函数。4.3 Nginx 部署多 web 项目时的代理路径与 Service Worker 坑本地开发跑npm run dev没问题部署时如果一台服务器要跑多个 web 项目就需要在 Nginx 上做路径区分。比如前端打包产物放在/var/www/parking后端服务在8080配置可以按下面方式写。server { listen 80; server_name parking.example.com; location /parking/ { alias /var/www/parking/; try_files $uri $uri/ /parking/index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }alias后面的/不能丢try_files里的/parking/index.html要和alias路径对应否则刷新二级页面就直接 404。proxy_pass不带末尾斜杠请求/api/v1/entry会原样转发到后端后端接口定义也是以/api开头两边保持一致最省事。另一个常被翻出来的问题是浏览器控制台报could not register service worker: InvalidStateError。这个错误多数出现在 PWA 的service-worker.js被放在错误路径或者页面协议不是 https/localhost。处理办法很简单给注册逻辑加一个保护或者干脆不在管理后台启用 PWA。if (serviceWorker in navigator window.isSecureContext) { navigator.serviceWorker.register(/sw.js).catch(() { console.warn(service worker 注册失败不影响页面使用) }) }window.isSecureContext会在非 HTTPS 环境下返回false这样内网 HTTP 调试时就不会抛出异常也不会把整个页面的加载流程卡住。5. 上线前用 curl 脚本把停车场管理系统的入场、计费、离场跑通5.1 一个订单流脚本验证幂等和费用计算部署完成后先别打开浏览器手工点用脚本连续跑几遍验证完整闭环。下面这段 bash 脚本可以直接复制到服务器上BASEhttp://127.0.0.1:8080 # 第 1 次正常入场 curl -s -X POST $BASE/api/v1/entry \ -H Content-Type: application/json \ -d {plateNo:京A12345,spaceCode:A01} # 第 2 次重复入场应返回“该车辆已在场内” curl -s -X POST $BASE/api/v1/entry \ -H Content-Type: application/json \ -d {plateNo:京A12345,spaceCode:A01} # 第 3 次离场orderNo 固定便于重试 curl -s -X POST $BASE/api/v1/exit \ -H Content-Type: application/json \ -d {plateNo:京A12345,orderNo:test-order-001} # 第 4 次用同一个 orderNo 再离场一次应返回同一订单而非新订单 curl -s -X POST $BASE/api/v1/exit \ -H Content-Type: application/json \ -d {plateNo:京A12345,orderNo:test-order-001}第 2 次调用如果返回了入场成功说明幂等没做好第 4 次调用如果生成了新订单说明重复扣费的 bug 已经存在。两次调用之间要把数据库的parking_order表查一下确认只有一条order_no test-order-001的记录。5.2 正式数据里找出问题的三个检查点系统跑起来之后异常不只在测试时出现日常运维要会从订单表反查。下面是三组最常见的检查点。现象查哪里可能原因与处理车位显示被占用但订单已经支付car_space的update_time与parking_record.exit_time事务未提交或离场接口中途异常把spaceDao.release移到Transactional方法内同一辆车出现两条在停记录parking_record的plate_no与exit_time入口重复抬杆或接口并发补唯一索引或在业务层二次校验结算金额与收费规则差 0.5 元fee_rule的cycle_minutes与first_minutes向上取整公式在免费时长边界算错用边界分钟数写单测运维时给parking_record和parking_order各加一个定时任务做一致性检查扫描exit_time不为空但order_id为空的记录以及order_id有值但amount为零的订单。这类任务放在凌晨低峰期执行输出结果到一张check_log表第二天由管理人员确认。web 停车场管理系统的稳定性最后靠的不是界面华丽而是这些能自动对账的边界检查。本文还有配套的精品资源点击获取
