
简介这是一份基于Web的停车场管理系统毕业设计完整项目资源面向计算机相关专业学生及需要完成Java Web课程设计或毕设开发的初学者。压缩包约116.2MB包含Java源码、数据库脚本、开题报告与论文文档、视频操作说明等交付内容其中数据库脚本便于快速建库建表视频说明可辅助理解部署与运行流程。系统功能覆盖系统信息管理、车位信息管理、IC卡信息管理、固定车主停车管理、临时车辆信息管理及系统功能操作六大模块支持角色与用户的增删改查、车位与IC卡维护、固定车主及临时车辆停车记录管理、修改密码与退出登录等操作模块划分清晰适合作为毕业设计选题拆解、数据库表关系梳理、功能设计与答辩准备的直接参考。目前已有124人学习浏览代码结构完整便于在此基础上进行二次开发与功能扩展。1. 停车场管理系统用 Web 重做一遍毕设选题背后的真实工作量如果你在找“基于Web停车场管理系统的设计与实现”大概率是计算机专业在做毕业设计或课程设计手上这个 zip 里装着 Java 源码、数据库脚本、开题报告论文文档和视频说明。这套东西的本质是一个典型的“管理信息系统”作业前端网页负责录入和展示后端 Java 代码处理业务逻辑MySQL 存数据把停车场的车辆进出、车位占用、收费记录这些动作变成能在浏览器里操作的界面。这类项目的真实工作量不在“停车”本身而在三件事把车辆进出和计费规则建模成数据库表结构把页面表单和数据库操作通过后端接口串起来再把整个项目从 IDE 里搬到 Tomcat 上跑通。言语间很多同学在答辩前一周才发现自己卡在了环境配置上而不是代码上。这篇笔记按“业务建模 → 数据库设计 → 后端分层 → 前端联动 → 部署验证”的顺序把每个环节的做法、参数和坑讲清楚适合需要复现、改造或答辩讲解的人直接照做。2. 先拆业务再写代码车辆进出、计费与车位的三张核心表2.1 从“停车”到“计费”的流程建模停车场管理系统的核心业务可以拆成两条线一条是车辆生命周期从进场登记到出场结算另一条是车位状态变化从空闲到占用再回到空闲。大多数毕设版本的流程是这样车主进场时管理员在网页上登记车牌号系统记录入场时间并把对应车位标记为占用车主出场时管理员根据车牌查出入场记录系统按计费规则算出金额记录收费后释放车位。这里最容易犯的错是只建一张“车辆表”存所有字段。正确做法是把一次停车行为拆成“车辆档案”和“停车记录”两张表车辆档案只存车牌、车主姓名、电话、车辆类型这些不变信息停车记录存每次入场的车牌、入场时间、出场时间、应收金额、实收金额、状态。言语间把不变信息和一次行为分开后面做计费统计、车位占用分析、历史记录查询都会轻松很多这也是答辩时老师爱问的“为什么这么设计表结构”的标准答案。表结构之外计费规则也要在动手写代码前定死。常见做法是分两类按时收费和按次收费。按时收费需要设计单位时间价格和不足一小时的取整方式按次收费则简单进场固定金额。如果你拿到手的源码里计费逻辑写在 JSP 页面里建议你把它挪到 Java 的 Service 层不然改一次价格要翻遍所有页面。2.2 建表 SQL字段类型、长度与索引的三个关键选择数据库脚本是整个项目的地基地基不对后面全是返工。以下是按典型需求设计的核心建表语句包括车位表、车辆表和停车记录表CREATE TABLE parking_space ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 车位ID, space_no VARCHAR(20) NOT NULL UNIQUE COMMENT 车位编号如A-01, location VARCHAR(50) COMMENT 区域位置描述, status TINYINT DEFAULT 0 COMMENT 0空闲 1占用, vehicle_id INT COMMENT 当前占用车辆ID可为空 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车位表; CREATE TABLE vehicle ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 车辆ID, plate_no VARCHAR(20) NOT NULL UNIQUE COMMENT 车牌号, owner_name VARCHAR(50) COMMENT 车主姓名, phone VARCHAR(20) COMMENT 联系电话, vehicle_type TINYINT DEFAULT 0 COMMENT 0临时车 1月租车 2免费车 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车辆档案表; CREATE TABLE parking_record ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 记录ID, plate_no VARCHAR(20) NOT NULL COMMENT 车牌号冗余存储便于查询, space_id INT COMMENT 占用车位ID, entry_time DATETIME NOT NULL COMMENT 入场时间, exit_time DATETIME COMMENT 出场时间未出场为NULL, fee DECIMAL(10,2) DEFAULT 0 COMMENT 应收金额, paid TINYINT DEFAULT 0 COMMENT 0未收费 1已收费, INDEX idx_plate_entry (plate_no, entry_time), INDEX idx_status_time (paid, entry_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT停车记录表;车牌号在 vehicle 表里设为 UNIQUE 的原因是同一辆车在系统里只保留一份档案临时车第一次进场自动建档之后进出都查这份档案。parking_record 里冗余一个 plate_no是为了避免每次查记录都要 JOIN vehicle 表——查询频繁的字段直接冗余这是数据库设计的常见取舍。boundary 参数说明一下entry_time 用 DATETIME 而不用 TIMESTAMPDATETIME 范围到 9999 年而且不受时区影响适合做停车计费这种需要精确时间点的业务status 和 paid 用 TINYINT 而不是 VARCHAR 存“是/否”省空间而且查询快。索引设计上idx_plate_entry 支撑“按车牌查历史停车记录”这个高频查询idx_status_time 支撑“统计某段时间内已收费/未收费的记录”。如果后期数据量大可以在 entry_time 上单独建索引但毕设规模下这两个联合索引足够。所有表加 ENGINEInnoDB是因为计费场景涉及金额事务回滚能力必须有。2.3 JDBC 连接参数URL、驱动与连接池的配置细节数据库表建好后Java 后端要连上它。大多数毕设项目用 JDBC C3P0 或 Druid 连接池配置文件里最要命的是参数配错导致中文乱码。下面是一个常用的 Druid 配置jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/parking_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456 jdbc.initialSize5 jdbc.maxActive20 jdbc.maxWait60000useUnicodetruecharacterEncodingutf8这两个参数缺失页面传入的中文车牌号会变成问号车辆类型显示成乱码。useSSLfalse是因为本地开发环境通常没配 SSL 证书加上这个参数避免连接时的证书校验报错。serverTimezoneAsia/Shanghai不加高版本 MySQL 驱动会报时区错误。连接池的maxActive20对毕设足够如果做压力测试发现连接不够用先看是不是没关连接而不是加连接数。3. 后端分层实现DAO 操作、Service 事务与计费规则的落地3.1 从 JSP 直连数据库到三层结构的改造逻辑初学者拿到的源码很多是 JSP 页面里直接写 JDBC 代码查询、更新、业务判断全挤在一起。这种写法演示可以但答辩时老师问“如果计费规则变了你改哪个文件”就会卡住。我一般会把代码拆成三层Servlet 接收请求和参数校验Service 层放业务规则如计费计算DAO 层只做数据库读写。这样改计费规则只动 Service换数据库只动 DAO页面只负责展示。对于已经写好的 JSP 直连代码改造步骤是先把数据库连接操作抽到 DBUtil 类里再把每个页面的 SQL 语句抽到对应实体类的 DAO 方法中最后把业务判断如“停车超过一小时怎么收费”挪到 Service。如果你打算自己做直接按三层结构从零写比改旧代码省事。3.2 车辆入场与出场DAO 方法、状态流转与事务边界以下是车辆入场和出场两个核心操作的代码实现入场时登记并占用车位出场时计算费用并释放车位。这里把 DAO 方法和 Service 层的事务控制放在一起看// ParkingRecordDAO.java public int insertEntryRecord(ParkingRecord record) throws SQLException { String sql INSERT INTO parking_record (plate_no, space_id, entry_time, paid) VALUES (?, ?, NOW(), 0); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, record.getPlateNo()); ps.setInt(2, record.getSpaceId()); ps.executeUpdate(); ResultSet rs ps.getGeneratedKeys(); if (rs.next()) return rs.getInt(1); return 0; } } public int updateExitRecord(int recordId, BigDecimal fee) throws SQLException { String sql UPDATE parking_record SET exit_time NOW(), fee ?, paid 1 WHERE id ? AND exit_time IS NULL; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setBigDecimal(1, fee); ps.setInt(2, recordId); return ps.executeUpdate(); } }// ParkingService.java public void vehicleEntry(String plateNo, int spaceId) throws Exception { Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); // 1. 查车位是否被占用 ParkingSpaceDAO spaceDAO new ParkingSpaceDAO(); ParkingSpace space spaceDAO.findById(spaceId); if (space null || space.getStatus() 1) { throw new RuntimeException(车位不存在或已被占用); } // 2. 查车辆档案不存在则自动建档 VehicleDAO vehicleDAO new VehicleDAO(); Vehicle vehicle vehicleDAO.findByPlateNo(plateNo); if (vehicle null) { vehicle new Vehicle(); vehicle.setPlateNo(plateNo); vehicle.setVehicleType(0); // 默认临时车 vehicleDAO.insert(vehicle); } // 3. 插入停车记录并占用车位 ParkingRecordDAO recordDAO new ParkingRecordDAO(); int recordId recordDAO.insertEntryRecord(new ParkingRecord(plateNo, spaceId)); spaceDAO.updateStatus(spaceId, 1, vehicle.getId()); conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); } }逻辑说明vehicleEntry里的核心是事务边界三个步骤要么全部成功要么全部回滚如果第 2 步建档成功但第 3 步插入记录失败事务回滚后车辆档案也不会存在避免出现“有档案没记录”的孤儿数据。updateExitRecord的 SQL 里加了AND exit_time IS NULL这是边界参数的关键——防止同一个记录被重复结算用户连点两次出场按钮也不会重复收费。Statement.RETURN_GENERATED_KEYS用于获取自增主键后续业务需要拿着这个 ID 更新出场信息时用得上。3.3 计费规则字符串时间差计算与月租车判断计费是答辩必问的功能很多源码里的实现是先把时间转成字符串再截取小时数误差很大。正确做法是直接计算两个 Date 对象的毫秒差再按规则换算public BigDecimal calcFee(Date entryTime, Date exitTime, int vehicleType) { long diffMillis exitTime.getTime() - entryTime.getTime(); BigDecimal hours new BigDecimal(diffMillis).divide(new BigDecimal(3600000), 2, RoundingMode.CEILING); // 月租车和免费车不收费 if (vehicleType 1 || vehicleType 2) { return BigDecimal.ZERO; } // 临时车首小时5元之后每小时3元不足一小时按一小时算 BigDecimal fee BigDecimal.valueOf(5); if (hours.compareTo(BigDecimal.ONE) 0) { BigDecimal extraHours hours.subtract(BigDecimal.ONE).setScale(0, RoundingMode.CEILING); fee fee.add(extraHours.multiply(BigDecimal.valueOf(3))); } return fee; }参数说明RoundingMode.CEILING在这里是向上取整停车 1 小时 1 分钟按 2 小时收费这是线下停车场的通用规则。用BigDecimal而不是double计算金额是财务计算的基本要求double 的浮点误差在累计统计时会被放大。月租车判断放在计费方法的最前面避免临时车的计费规则误伤包月用户。这个方法的调用时间点要放在出场操作里拿到exitTime之后立刻算。4. 管理端页面与交互表单校验、车位状态实时刷新与后端接口对接4.1 页面功能拆分入场登记、出场结算、车位监控与记录查询管理端的 Web 页面通常包含四个功能模块入场登记页面是一个表单输入车牌号、选择车位号提交后调后端接口出场结算页面输入车牌号查出未出场记录显示入场时间和当前费用确认后结算车位监控页面用表格展示当前所有车位状态和占用车辆记录查询页面按车牌或时间段查历史停车记录和收费统计。设计页面时入场登记和出场结算这两个页面的表单校验要区分开入场时车牌号必须匹配“省份简称字母数字”的常见格式出场时重点是查无记录时给出明确提示而不是让用户看到一个空表不知所措。4.2 用 JavaScript 做无刷新判断的交互写法表单提交环节比 Form 直接 submit 更顺手的是用 fetch 调用后端接口页面不刷新错误信息直接显示在表单下方。下面是一段入场登记的 JavaScript 代码串起了前端校验和后端请求document.getElementById(entryForm).addEventListener(submit, function(e) { e.preventDefault(); const plateNo document.getElementById(plateNo).value.trim(); const spaceId document.getElementById(spaceId).value; // 车牌号格式校验简单的省份简称字母数字模式 const platePattern /^[\u4e00-\u9fa5][A-Z][A-Z0-9]{4,5}$/; if (!platePattern.test(plateNo)) { document.getElementById(msg).textContent 车牌号格式不正确示例京A12345; return; } fetch(entryServlet, { method: POST, headers: { Content-Type: application/x-www-form-urlencoded }, body: plateNo encodeURIComponent(plateNo) spaceId encodeURIComponent(spaceId) }) .then(res res.json()) .then(data { if (data.code 200) { // 入场成功后刷新车位状态区域 loadParkingStatus(); document.getElementById(msg).textContent 入场登记成功; } else { document.getElementById(msg).textContent data.message; } }) .catch(err { document.getElementById(msg).textContent 请求失败请检查网络后重试; }); });车牌正则^[\u4e00-\u9fa5][A-Z][A-Z0-9]{4,5}$覆盖了绝大多数蓝牌和绿牌的格式但新能源六位牌和部分特殊牌照会漏掉所以这个校验是前端友好提示真正的合法性判断还是要在后端做一次。encodeURIComponent对中文车牌做编码是必须的不编码的话包含中文的请求体会在部分容器上出现乱码。e.preventDefault()阻止了表单默认提交请求改走 fetch这就是无刷新交互的关键。代码里loadParkingStatus()是单独封装的函数入场后调用它可以同步车位监控区的状态这是页面局部刷新的常见做法。4.3 车位状态更新的两种常见方案刷新整表与 AJAX 局部刷新车位监控页面有两种状态更新方式。第一种是定时整页刷新用meta http-equivrefresh content10实现简单但每次都闪一下体验差而且用户在操作弹窗时会被强制刷新打断。第二种是 AJAX 定时拉取页面加载后启动setInterval每 10 秒调用一次查询接口拿到数据后更新表格区域不闪屏对服务器的压力也远小于整页刷新。我建议用第二种方案因为停车场的状态变化频率不高10 秒的轮询间隔足够实时性。轮询接口返回的数据格式建议是 JSON结构像{code:200, data:[{id:1, spaceNo:A-01, status:0}, ...]}前端遍历后根据 status 字段决定单元格显示“空闲”还是“占用”以及用什么颜色。如果你的源码里是整页刷新答辩时提一句“用轮询替代 meta refresh 是为了避免操作过程中页面重载”是加分项。5. 部署与联调Tomcat 运行、MySQL 初始化、中文乱码与端口冲突排查5.1 把项目跑起来的三个前置检查项JDK 版本、Tomcat 版本、MySQL 版本拿到源码后第一件事不是急着打开 IDE而是检查环境版本。JDK 8 是绝大多数毕设项目的编译目标如果你本机装了 JDK 17 打开一个 JDK 8 编译的项目大概率会遇到一版警告和兼容问题。Tomcat 推荐 8.5 或 9如果源码里的 Servlet 依赖的是 javax.servlet 包Tomcat 10 用的是 jakarta.servlet 包直接换容器会导致编译错误。MySQL 建议 5.7 或 8.x导入数据库脚本时留意脚本开头的版本注释有些旧脚本用了ENGINEMyISAM或过期语法需要在导入前处理。检查项可以用一条命令快速验证java -version # 需要看到 1.8.x 的输出 mysql --version # 需要看到 5.7.x 或 8.x如果你机器上有多个 JDK 版本IDE 里的 Project Structure 和 Tomcat 的 JRE 配置都要指到同一个 JDK。version 参数检查完毕再导入数据库脚本往下走就不会在环境上花太多时间。5.2 MySQL 导入与连接验证source 命令和 Navicat 两条路径数据库脚本导入是部署的重头戏。拿到手的 .sql 文件通常在 zip 的数据库目录里打开看第一行有没有CREATE DATABASE有的话直接在命令行执行mysql -uroot -p /path/to/parking_db.sql如果脚本里没有建库语句需要先手动建库再指定数据库导入CREATE DATABASE IF NOT EXISTS parking_db DEFAULT CHARSET utf8mb4; USE parking_db; SOURCE /path/to/parking_db.sql; SHOW TABLES;SHOW TABLES输出里应至少包含车辆、停车记录、车位这三张核心表说明导入成功。用 Navicat 或 DataGrip 导入也可以但注意编码选择要匹配脚本文件的编码格式通常脚本文件是 UTF-8如果打开乱码用记事本另存为 UTF-8 后再导入。导入完后用下面的 SQL 验证一下数据完整性SELECT COUNT(*) FROM vehicle; SELECT COUNT(*) FROM parking_record; SELECT COUNT(*) FROM parking_space;如果记录表数据明显不对先检查是不是导入时漏了部分表。连接数据库的代码里配置的用户名和密码必须和本地 MySQL 实际账号一致很多人改了数据库密码后只改了 Navicat 里的连接信息没改项目里的 jdbc.properties导致 Tomcat 启动后页面报数据库连接失败。5.3 中文乱码、端口 8080 被占用、数据库驱动冲突的三个翻车现场部署阶段的坑我挑三个最常见的写在这里。第一个是中文乱码现象是页面上所有中文都显示成“”原因通常是 MySQL 连接 URL 里少了characterEncodingutf8或者项目里的 JSP 页面没加pageEncodingUTF-8。解决方法是先检查 jdbc.properties 的 URL再确认所有 JSP 头部的contentTypetext/html; charsetUTF-8是否写全。第二个是端口 8080 被占用现象是 Tomcat 启动时控制台报Port 8080 required by Tomcat v9.0 Server at localhost is already in use用netstat -ano | findstr 8080找到占用进程要么关掉那个进程要么改 Tomcat 的 server.xml 里的端口号。第三个是数据库驱动版本和 MySQL 版本不匹配比如用了旧版 mysql-connector-java 5.x 驱动连 MySQL 8.x启动时驱动类加载不到。检查 lib 里的驱动 jar 版本推荐用 mysql-connector-java 8.0.x 或者直接改用现成的 mysql-connector-j。这三个问题占了部署报错的八成定位顺序按“先看日志再查配置最后查端口和驱动”来走。5.4 验证接口通不通用 curl 和浏览器控制台快速定位问题层项目启动后用浏览器打开登录页是最直观的判断但页面出来不代表接口都通。建议用 curl 直接测后端的接口层比如入场登记的 Servletcurl -X POST http://localhost:8080/parking/entryServlet \ -d plateNo京A12345spaceId1 \ -w \nHTTP状态码: %{http_code}\n如果返回 JSON 里有code:200说明从浏览器到 Servlet 再到数据库这条路是通的。如果返回 404检查项目部署名的 context path 是不是parking。如果返回 500去 Tomcat 的 logs 目录看 localhost 日期.log 里的堆栈信息大部分问题都是 SQL 语法或字段类型不匹配。浏览器控制台里 Network 面板看请求状态红色条目代表请求失败了点进去看响应体里有没有具体的报错文案。这种从外到里的排查顺序能帮你快速判断问题在渲染层、接口层还是数据层。6. 答辩前必做的验证清单与加大分项从数据统计到权限控制6.1 用三组场景跑通核心链路入场、出场、非法数据答辩演示最怕演示到一半流程断了。进场前先按这三组线性流程走一遍第一组是临时车从入场到出场完整流程确认计费正确、状态释放、记录生成第二组是月租车入场出场核对费用为 0第三组是输入不存在的车牌号出场确认系统给出提示而不是报错。每组场景都盯着数据库里的变化看入场后SELECT * FROM parking_record WHERE exit_time IS NULL应有一条该车牌的记录且状态为 0未收费出场后那条记录的exit_time不为空且paid1对应车位表的status恢复为 0。6.2 给系统加两个低成本高回报的功能登录拦截与收费统计如果想让项目从及格变优秀最值得加的是登录权限拦截和收费统计报表。登录拦截可以用 Servlet 的 Filter 实现未登录的请求转发到登录页这个功能代码量不到二十行但体现了你对会话管理的理解。收费统计报表是在记录查询页加一个按日期的分组汇总SQL 大概是SELECT DATE_FORMAT(entry_time, %Y-%m-%d) AS day, SUM(fee) AS total_fee FROM parking_record WHERE paid 1 GROUP BY DATE_FORMAT(entry_time, %Y-%m-%d) ORDER BY day DESC;这个查询演示了 MySQL 日期格式化函数和 GROUP BY 聚合的使用答辩时能主动展示一段自己写的统计分析 SQL比单纯演示页面操作有说服力得多。6.3 最后的自查习惯数据库脚本重新导入一遍项目从零部署一遍答辩前一天的夜晚我建议你把数据库删掉重新导入一遍项目从 IDE 里删掉 Tomcat 重新 add 一遍。这个动作不复杂却能把“我电脑上本来是好的”这种风险消灭在答辩之前。跑通之后把数据库脚本、部署步骤、测试账号这三样东西单独放一个文档放到项目根目录。演示那天老师和同学用的是另一台电脑你能在五分钟内让项目跑起来这本身就给整个项目定了调。希望帮到你按这套顺序走下来这套停车场管理系统从能跑变成能讲、能经得住问。本文还有配套的精品资源点击获取