
简介这是一份基于Web的停车场管理系统设计与实现的完整文档适合计算机相关专业学生、毕业设计者及初级开发人员参考。文档围绕城市停车难题详细阐述了系统的研究背景、目的意义与国内外现状并给出了基于Java、Spring Boot、MySQL及前端HTML5/CSS3/JavaScript的具体开发方案。系统设计采用MVC架构与模块化思想涵盖用户管理、车位管理、计费管理、预约管理等功能模块并涉及RESTful API、AJAX、JWT、GIS等关键技术有助于读者理解Web系统从需求分析到设计与实现的完整流程。资源为单个docx文件包体大小约947KB内容结构清晰包含系统总体设计、数据库概念与逻辑结构设计、业务流程设计等章节可直接用作毕业设计或课程报告的写作参考。目前已有134人学习浏览具备一定的参考价值。1. 从车位台账到 Web 化这套停车场系统在解决什么问题高峰期的停车场管理员要同时处理三件事给临时车算钱、查月租车的车位、更新手工台账。任何一个环节出错都会变成出场口的排队和车主投诉。这套基于 Web 的停车场管理系统把「固定车主」和「临时车辆」两条业务线分别建模——固定车主用 IC 卡绑定车位按周结算临时车辆按停留时间计费出场时统一结算。整个前端用 JSP 动态生成页面后端用 Java Servlet 处理业务数据落在 MySQL 上典型的三层结构。对想复现 JSP Servlet MVC 老项目的开发者或者需要给中小停车场做信息化改造的工程师这套系统的表结构和计费逻辑都有直接能抄的部分。2. B/S 架构与 MVC 分层JSP Servlet 在 Tomcat 上的运行机制2.1 B/S 与 C/S 的选型取舍停车场管理系统的使用者分两类一类是前台岗亭的收费员一类是办公室做月租登记和报表的管理员。这两类用户不在同一个物理位置如果采用 C/S 结构每台收费电脑都要装客户端版本升级时得一台一台去维护而 B/S 结构下客户端只要一个浏览器系统发布到 Tomcat 后所有用户访问服务器上的同一份代码。这也是这套系统选择 B/S 的根本原因——部署成本低升级只动服务器。对比项C/S 结构B/S 结构客户端安装每台机器安装独立客户端浏览器直接访问系统升级逐台更新服务器更新一次网络依赖局域网为主局域网/广域网均可适合场景固定工位、高交互桌面应用多网点、多角色的管理系统代价是 B/S 对网络和服务端并发能力更敏感。高峰期多台岗亭同时出场结算时Tomcat 默认线程池和数据库连接池会成为瓶颈这个在后面的性能调优部分会专门提到。2.2 MVC 三层在停车场景中的落地划分这套系统的后台采用 MVC 开发模式核心目的是降低代码耦合度、提高代码重用性。具体到代码层面JSP 作为视图层只负责展示数据不在页面里写业务逻辑Servlet 作为控制层接收请求、解析参数、调用业务方法、决定跳转到哪个 JSP模型层由 JavaBean 和 DAO 组成负责访问 MySQL 数据库。一个典型的「添加车位」请求的流转过程是这样的管理员在 add_seat.jsp 页面填写车位编号、所属区域表单提交到 SeatServletSeatServlet 调用 SeatDao 的 insert 方法将数据写入 seat 表写库成功后 Servlet 转发到 seat_list.jsp 渲染最新列表。这样做的直接好处是后期如果要换数据库只需要改 DAO 层的实现JSP 和 Servlet 完全不用动。2.2.1 请求流转与编码处理的代码骨架下面这段是控制层 Servlet 的骨架代码覆盖了参数接收、状态判断和页面转发三个动作// SeatServlet.java 的部分实现 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); // 解决中文参数乱码 String seatNum request.getParameter(seat_num); String seatArea request.getParameter(seat_area); String seatState request.getParameter(seat_state); Seat seat new Seat(); seat.setSeatNum(seatNum); seat.setSeatArea(seatArea); seat.setSeatState(seatState); SeatDao dao new SeatDao(); boolean ok dao.insertSeat(seat); if (ok) { // 成功后重定向到列表页避免刷新表单重复提交 response.sendRedirect(seat_list.jsp); } else { request.setAttribute(msg, 新增车位失败请检查车位编号是否重复); request.getRequestDispatcher(add_seat.jsp).forward(request, response); } }需要特别留意sendRedirect和forward的区别重定向是浏览器重新发起一次请求地址栏会变成目标地址适合写操作完成后跳转转发发生在服务端内部地址栏不变适合携带错误信息回显到原表单页。这段代码里两个动作都用到了对应的是两种不同的交互场景。另一个细节是request.setCharacterEncoding(UTF-8)必须放在读取任何参数之前执行否则中文车位区域名会变成乱码。2.3 MyEclipse Tomcat MySQL 环境配置要点原项目基于 MyEclipse2014 JDK Tomcat 构建对复现者来说最省事的做法是保持三层版本匹配JDK 1.7/1.8 配 Tomcat 7/8MySQL 5.5 以上即可。MyEclipse 里把项目右键 Run As - Run on Server本质上就是把打好的 war 包丢给 Tomcat 的 webapps 目录。Tomcat 默认监听 8080 端口如果本机有其他服务占用需要手动改 conf/server.xml!-- conf/server.xml将 8080 换成未占用的端口 -- Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 /数据库连接方面早期 JSP 项目最常见的写法是 JDBC DriverManager连接串里必须显式指定字符集否则后面所有中文查询都会出问题Class.forName(com.mysql.jdbc.Driver); String url jdbc:mysql://localhost:3306/parking ?useUnicodetruecharacterEncodingutf8; Connection conn DriverManager.getConnection(url, root, 123456);useUnicodetrue和characterEncodingutf8两个参数要一起写只写其中一个时MySQL 驱动可能仍按默认的 latin1 处理字符串导致入库的中文变成问号。JDBC 驱动 jar 文件要放到 WEB-INF/lib 目录下Tomcat 启动时会自动加载如果驱动放错位置运行时会报 ClassNotFoundException。3. 数据库设计六张核心表如何支撑固定车主与临时车辆两条业务线3.1 概念结构六个实体与 E-R 主线数据库设计是否合理直接决定后期功能扩展的难度。这套系统的实体一共六个用户、角色、IC 卡、车位、固定车主停车记录、临时车辆停车记录。实体之间的关系并不复杂角色和用户是一对多关系一个角色下挂多个用户IC 卡与车位是一对一关系一张卡固定绑定一个车位IC 卡与固定停车记录是一对多关系同一张卡多次入场产生多条记录临时车辆则不绑定固定车位入场时分配车位、出场时结算。E-R 设计里最容易出错的地方是「固定车辆」和「临时车辆」的建模差异。固定车辆的核心是 IC 卡车牌只是 IC 卡信息表里的一个属性临时车辆没有固定 IC 卡的概念入场时记录临时卡号、车牌号、入场时间。如果把这两类放在一张表里会出现大量空字段而且计费逻辑会被迫用类型字段去分支判断后续改价就麻烦了。3.2 五张核心表的 DDL 与字段说明原系统数据库基于 MySQL 5.5 构建建表语句可以直接在本地复现。下面是五张核心表的 DDL-- 用户表绑定角色控制后台登录权限 CREATE TABLE user ( user_id INT(11) NOT NULL AUTO_INCREMENT, role_id INT(11) NOT NULL COMMENT 角色 id关联 role 表, user_name VARCHAR(50) DEFAULT NULL COMMENT 登录名, real_name VARCHAR(50) DEFAULT NULL COMMENT 真实姓名, user_pwd VARCHAR(50) DEFAULT NULL COMMENT 登录密码, user_phone VARCHAR(50) DEFAULT NULL COMMENT 联系电话, PRIMARY KEY (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT用户表; -- 角色表系统信息管理模块的权限基础 CREATE TABLE role ( role_id INT(11) NOT NULL AUTO_INCREMENT, role_name VARCHAR(50) DEFAULT NULL COMMENT 角色名称, role_desc VARCHAR(50) DEFAULT NULL COMMENT 角色描述, PRIMARY KEY (role_id) ) ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT角色表; -- 车位信息表 CREATE TABLE seat ( seat_id INT(11) NOT NULL AUTO_INCREMENT, seat_num VARCHAR(50) DEFAULT NULL COMMENT 车位编号, seat_area VARCHAR(50) DEFAULT NULL COMMENT 所属区域, seat_state VARCHAR(50) DEFAULT NULL COMMENT 车位状态空闲/占用, seat_tag VARCHAR(50) DEFAULT NULL COMMENT 车位备注, PRIMARY KEY (seat_id) ) ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT车位信息表;IC 卡表、固定停车表和临时停车表的 DDL 如下-- IC 卡信息表固定车主、车牌、车位绑定关系都在这里 CREATE TABLE ic_card ( card_id INT(11) NOT NULL AUTO_INCREMENT, seat_id INT(11) NOT NULL COMMENT 绑定车位, user_name VARCHAR(50) DEFAULT NULL COMMENT 车主姓名, user_sex VARCHAR(50) DEFAULT NULL COMMENT 性别, user_addr VARCHAR(50) DEFAULT NULL COMMENT 家庭住址, car_num VARCHAR(50) DEFAULT NULL COMMENT 车牌号, PRIMARY KEY (card_id) ) ENGINEInnoDB DEFAULT CHARSETutf8 COMMENTIC 卡信息表; -- 固定车主停车记录表按周计费entry/out 时间对 CREATE TABLE fixed_parking ( fixed_id INT(11) NOT NULL AUTO_INCREMENT, car_id INT(11) NOT NULL COMMENT IC 卡 id, entry_date VARCHAR(50) DEFAULT NULL COMMENT 入场日期, entry_time VARCHAR(50) DEFAULT NULL COMMENT 入场时间, out_date VARCHAR(50) DEFAULT NULL COMMENT 出场日期, out_time VARCHAR(50) DEFAULT NULL COMMENT 出场时间, fixed_money VARCHAR(50) DEFAULT NULL COMMENT 停车费用, PRIMARY KEY (fixed_id) ) ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT固定车主停车表; -- 临时车辆停车记录表分钟级计费 CREATE TABLE temp_parking ( temp_id INT(11) NOT NULL AUTO_INCREMENT, car_id INT(11) DEFAULT NULL COMMENT 临时 IC 卡 id, car_num VARCHAR(50) DEFAULT NULL COMMENT 车牌号码, entry_date VARCHAR(50) DEFAULT NULL COMMENT 入场日期, entry_time VARCHAR(50) DEFAULT NULL COMMENT 入场时间, out_date VARCHAR(50) DEFAULT NULL COMMENT 出场日期, out_time VARCHAR(50) DEFAULT NULL COMMENT 出场时间, temp_money VARCHAR(50) DEFAULT NULL COMMENT 停车费用, PRIMARY KEY (temp_id) ) ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT临时停车表;字段设计上有一个容易忽略的点入场和出场时间被拆成了 entry_date / entry_time 两个 VARCHAR(50) 字段而不是一个 DATETIME。这种设计的好处是页面展示时可以直接拼接成「2025-04-10 08:30」的文本省去了 SimpleDateFormat 转换坏处是时间运算必须先拼接字符串再解析SQL 里做区间统计会麻烦一些。如果是在新项目里复刻这套表建议直接用 DATETIME 类型开发时少踩很多解析的坑。3.2.1 用户表与角色表的权限联动user 表的role_id指向 role 表的role_id。登录成功后后台把当前用户和角色名放进 Session菜单展示时按角色名过滤。这种设计是所有 JSP 后台管理系统里最朴素的 RBAC 实现——没有给用户分配角色的中间表一个用户只属于一个角色胜在简单够用。3.2.2 六张表职责速查表名核心字段对应模块useruser_id, role_id, user_name, user_pwd系统信息管理rolerole_id, role_name系统信息管理seatseat_id, seat_num, seat_area, seat_state车位信息管理ic_cardcard_id, seat_id, car_numIC 卡信息管理fixed_parkingfixed_id, car_id, entry_date, out_date固定车主停车管理temp_parkingtemp_id, car_id, car_num, entry_date临时车辆停车管理有了这个映射后面开发任何新功能时先想清楚它落在哪张表上再想怎么改表结构而不是直接加一个 JSP 页面了事。3.3 索引与字符集数据精确度怎么保障原系统提到「使用索引的方式加快数据查询速度并对各种查询条件进行优化来确保数据的精确度」。这句话拆开看是两件事第一查询走索引减少全表扫描第二字符集统一避免条件匹配因编码不一致落空。针对临时车辆出场结算的高频查询应该给车牌号和入场时间建索引ALTER TABLE temp_parking ADD INDEX idx_temp_car_num (car_num); ALTER TABLE temp_parking ADD INDEX idx_temp_entry_date (entry_date); -- 出场结算时按入场时间倒序取最近一条未出场记录 SELECT * FROM temp_parking WHERE car_num 京A12345 ORDER BY entry_date DESC, entry_time DESC LIMIT 1;注意索引生效的前置条件如果查询条件写成LIKE %车牌%前导通配符会让 MySQL 放弃该列的索引改用全表扫描。所以模糊搜索和精确匹配要分开设计——车牌精确查询走索引跨车场统计走 LIKE 也要尽量把通配符放在字符串右侧。字符集方面库、表、连接串三处都要统一为 utf8。如果建库时用了默认 latin1字段里存中文本来就会出问题连接串不带 characterEncoding 参数则中文条件查询即使表结构正确也可能查不到数据。排查这类问题可以先执行SHOW CREATE TABLE确认表字符集再检查 JDBC 连接串大多数乱码和查不到数据的根源都在这两处。4. 模块实现出入场、计费规则与查询导出的关键代码4.1 系统信息管理与菜单权限的控制系统信息管理模块的功能是用户管理和角色管理。登录认证通过后把用户对象和角色名写入 Session后续页面的菜单渲染根据角色名动态显示。管理员可以新增用户并指定角色但要注意一个安全细节修改密码和新增用户的接口必须分开避免普通用户通过构造请求直接创建管理员账号。// LoginServlet 中登录成功后的 Session 处理 User user userDao.findByUserNameAndPwd(userName, userPwd); if (user ! null) { HttpSession session request.getSession(); session.setAttribute(login_user, user); // 用 user.role_id 关联 role 表查出角色名控制菜单展示 Role role roleDao.findById(user.getRoleId()); session.setAttribute(role_name, role.getRoleName()); response.sendRedirect(index.jsp); } else { request.setAttribute(msg, 用户名或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); }把角色名放进 Session 而不是每次请求都查数据库是为了减少无谓的 SQL 查询。这个角色名只在登录时写入一次如果中途修改了角色需要重新登录才能生效对小型停车场管理系统来说这种延迟可以接受。4.2 固定车主按周计费400 元/周的上取整逻辑固定车主停车管理的核心是出入场记录和收费。原系统规则很明确按停留时间计算是否达到一周达到一周按 400 元/周累加收费。停 7 天收 400 元停 8 天按两周计算收 800 元。出场的 Java 逻辑可以这样写// 按周结算跨过 7 天即按下一周计费 SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd HH:mm); long entryMillis sdf.parse(entryDate entryTime).getTime(); long outMillis sdf.parse(outDate outTime).getTime(); long days TimeUnit.MILLISECONDS.toDays(outMillis - entryMillis); int weeks (int) Math.ceil(days / 7.0); if (weeks 1) { int money weeks * 400; // 达到一周弹出收费提示框 request.setAttribute(fee, money); request.setAttribute(msg, 该车辆停放已满 weeks 周应收 money 元); }Math.ceil(days / 7.0)是关键停 8 天时 days/7.0 约等于 1.14向上取整得到 2对应两周费用。如果你改造成本地上线时想改成「不满一周按天折算」可以把取整逻辑换成days * 400 / 7这属于停车场运营策略调整代码上只是改一行的事。固定车主和临时车辆的收费规则差异很大这里放一张对比表方便同时看两条业务线的差异对比项固定车主临时车辆身份标识IC 卡绑定车位和车主临时 IC 卡 车牌号计费周期按周计费400 元/周累加按分钟/小时计费出场操作设置出场时自动结算点击结算按钮后再出场数据归属fixed_parking 表temp_parking 表4.3 临时车辆分钟级计费时间差计算与状态控制临时车辆的流程是入口记录车牌号和入场时间出场时计算停留时长并生成费用。计算停留时间可以直接让 MySQL 算比用 Java 写日期差值代码少踩很多格式坑-- 出场结算时计算某辆车从入场到现在的停留分钟数 SELECT TIMESTAMPDIFF(MINUTE, STR_TO_DATE(CONCAT(entry_date, , entry_time), %Y-%m-%d %H:%i), NOW()) AS park_minutes FROM temp_parking WHERE car_num 京A12345 AND (out_time IS NULL OR out_time ) ORDER BY temp_id DESC LIMIT 1;TIMESTAMPDIFF的第一个参数可以是 MINUTE、HOUR、SECOND按精度需求选择。这里用STR_TO_DATE把字符串日期还原成时间类型是因为原表把日期和时间拆成了两个 VARCHAR(50) 字段如果按上一章建议改成 DATETIME这个 SQL 可以简化为TIMESTAMPDIFF(MINUTE, entry_time, NOW())。结算完成后需要执行 UPDATE 把出场时间写回去同时把车位状态改为空闲这两个操作应该放在同一个事务里避免车辆出场了但车位还显示占用。4.4 IC 卡联动查询与模糊搜索IC 卡信息管理模块支持按 IC 卡基本信息进行模糊查询同时出入场设置里选中 IC 卡号下拉框会自动带出车主信息。下拉联动在前端很简单监听 select 的 change 事件后从已加载的数据里取值回填// 选中 IC 卡号后自动回填车主姓名和车牌号 document.getElementById(cardId).onchange function () { var idx this.selectedIndex; var user this.options[idx].dataset.user; var car this.options[idx].dataset.car; document.getElementById(userName).value user; document.getElementById(carNum).value car; };模糊查询则要注意 SQL 注入风险。很多初学者会写成字符串拼接WHERE user_name LIKE % keyword %一旦 keyword 包含单引号就会破坏 SQL 结构。正确写法是使用 PreparedStatement 占位符// IC 卡模糊查询按车主姓名或车牌号匹配 String sql SELECT * FROM ic_card WHERE user_name LIKE CONCAT(%, ?, %) OR car_num LIKE CONCAT(%, ?, %); PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, keyword); ps.setString(2, keyword); ResultSet rs ps.executeQuery();CONCAT(%, ?, %)是 MySQL 里拼接模糊查询条件的推荐写法比直接传%keyword%可读性更好。PreparedStatement 会对参数里的特殊字符做转义从根上避免注入问题。这个规范在 JSP 老项目里同样适用不管用什么 DAO 框架占位符原则不能破。4.5 停车信息导出 Excel管理员可以将固定车主的停车信息导出为 Excel 表格这在 Java Web 项目里最常见的方案是 Apache POI。核心思路是查询出停车记录列表创建 HSSFWorkbook逐行写入数据最后把响应头设置为附件下载// 导出停车记录的简化代码 HSSFWorkbook workbook new HSSFWorkbook(); HSSFSheet sheet workbook.createSheet(固定车主停车信息); String[] headers {车牌号, 入场时间, 出场时间, 费用}; for (int i 0; i headers.length; i) { sheet.createRow(0).createCell(i).setCellValue(headers[i]); } // r 为 fixed_parking 表查询结果的一行记录 int rowIdx 1; for (ParkingRecord r : recordList) { HSSFRow row sheet.createRow(rowIdx); row.createCell(0).setCellValue(r.getCarNum()); row.createCell(1).setCellValue(r.getEntryDate() r.getEntryTime()); row.createCell(2).setCellValue(r.getOutDate() r.getOutTime()); row.createCell(3).setCellValue(r.getFixedMoney()); } // 设置响应头让浏览器以附件形式下载 response.setContentType(application/vnd.ms-excel); response.setHeader(Content-Disposition, attachment; filenameparking_records.xls); workbook.write(response.getOutputStream());导出功能的关键不在 POI API而在响应头设置。Content-Disposition里的attachment告诉浏览器这是下载行为而不是页面跳转filename指定默认文件名。如果导出中文文件名还要对 filename 做 URLEncoder 编码否则在部分浏览器里文件名会乱码。POI 生成的 .xls 文件兼容 Excel 2003 以上所有版本数据量在十万行以内性能没有问题需要导出大数据量时我一般会直接改成 CSV 输出或者换 SXSSFWorkbook 的流式写入内存占用会低很多。5. 测试用例与部署排错Tomcat、乱码、连接池这三个坎5.1 功能测试路径设计这套系统的测试重点放在三条链路上登录与权限、固定车辆周结算、临时车辆分钟结算。手工测试时先准备好一个测试停车场数据再按照下面这张表逐条执行测试项操作预期结果非法登录输入错误密码停留在登录页并提示用户名或密码错误固定车主入场选择 IC 卡号添加入场信息fixed_parking 新增记录出场时间为空固定车主出场停放不足 7 天点击设置出场不触发收费提示记录出场时间固定车主出场停放已满 7 天点击设置出场弹出收费提示金额按周累加临时车辆结算出场时点击结算temp_money 按停留分钟计算并写回停车信息导出点击导出 Excel下载文件包含最近的全部停车记录自动化测试方面老项目补测试可以用 JUnit 4 写 DAO 层的单元测试重点覆盖计费计算的时间边界比如 7 天整、7 天零 1 分钟、14 天整这三个用例。页面层用 Selenium 录制一遍登录取 Session 的路径就够了没有必要为这个规模的项目做全链路 UI 自动化。5.2 三个高频问题与处理第一个是 Tomcat 8080 端口被占用。启动时报Port 8080 required by Tomcat v8.0 Server at localhost is already in use先查占用进程再改端口# Windows 下查端口占用Linux/macOS 用 lsof -i:8080 netstat -ano | findstr 8080 taskkill /PID 进程号 /F如果端口不能释放就按第 2 章的方式改 server.xml 里的 Connector port。改完后重启 Tomcat日志里会打印新的端口号再访问 localhost:新端口 验证即可。第二个是 JSP 页面中文乱码。这种问题通常不是一处配置能解决的要检查四个位置JSP 顶部是否写了pageEncodingUTF-8、Servlet 里是否在读取参数前执行了request.setCharacterEncoding(UTF-8)、数据库连接串是否带characterEncodingutf8、表字符集是否为 utf8。四个位置统一后乱码才能彻底消失缺一个在特定页面或特定请求下都会复发。第三个是数据库连接超时与驱动加载失败。驱动 jar 必须放在 WEB-INF/lib 目录下Tomcat 启动后由类加载器自动加载而连接超时常见于空闲连接被 MySQL 关闭后连接池继续返回失效连接。如果用的是 Tomcat 内置连接池把maxIdle调小并加testOnBorrowtrue能降低拿到失效连接的概率。MySQL 侧的wait_timeout默认 8 小时部署在服务器上长期空闲的 Web 服务很容易触发这个问题优先从连接池侧解决。本文还有配套的精品资源点击获取