基于SSM+MySQL的停车场管理系统:计费状态与并发控制实战解析
简介基于SSMMySQL的停车场管理系统设计与实现资料包专为毕业设计、课程设计及Java Web学习者打造覆盖车辆进出管理、停车位监控、收费计费、统计报表等核心业务。资源内含项目全套源码、设计文档、部署说明和视频演示共1223个文件其中Java源码与JSP页面构成后端业务与动态视图JS、CSS及图片资源用于前端交互与界面展示JAR包为项目依赖库SQL脚本则完成数据库表结构与初始化数据整体压缩包42.73MB结构清晰便于直接导入IDE运行调试。系统采用SpringSpringMVCMyBatis框架整合MySQL分层设计与模块化封装有利于理解SSM协同原理及实际项目开发流程同时包含车辆进场出场记录、停车时长自动计费、车位状态监控、收入统计等模块数据库设计规范且考虑安全与扩展性。已有378人学习下载适合作为毕业设计参考或SSM项目练手借助配套文档和演示视频可快速核对功能实现与部署步骤节省从零搭建的时间。1. 基于SSMmysql的停车场管理系统难点其实在计费状态上SSM 框架配合 MySQL 做停车场管理系统听起来就是一套标准的业务 CRUD车位表、订单表、用户表配上增删改查。但真正跑过这类系统的人会有个共同感受——入场扫码很顺出场算钱时却容易对不上账。原因在于停车计费不是简单地把「入场时间」和「出场时间」相减乘单价它牵扯到场内车位状态如何在并发下保持正确、跨天跨时段怎么换费率、免费时长和封顶金额按什么优先级叠加。这些规则如果散落在 Service 层的 if-else 里改起来就会像打地鼠。这篇内容围绕 SSMSpring SpringMVC MyBatis与 MySQL 的组合从表结构设计、框架整合、状态机与并发控制一直讲到上线前的验证手段。适合两类人一类是拿它做课程设计或毕设另一类是在公司内部做小型停车管理工具、想少走弯路的一线开发。MySQL 相关的索引、存储过程、事务隔离级别会穿插在业务场景里讲不是孤立地罗列语法。2. 停车场业务拆解先想清车位、订单、计费规则这三类实体2.1 用表结构把「车位占用」和「计费订单」分开建模常见新手做法是一张表搞定一切车位表里挂当前入场时间、车牌号、应收金额。表面看字段少了、查询方便了但两个核心问题会立刻暴露。第一个问题是历史数据被覆盖车走了之后之前的停车记录没了月底统计车场营收只能靠导出 Excel 手工拼。第二个问题是并发安全两个保安同时在场口操作出场后写的人可能覆盖先写的人账单直接丢失。更稳妥的做法是把实时状态和流水数据分开建表。车位表只负责「这个车位现在是否空闲」真正计费时读的是订单表。下面的建表语句是这类系统里最常见的一组基础结构CREATE TABLE parking_space ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 车位ID, space_no VARCHAR(20) NOT NULL COMMENT 车位编号如 A-001, area VARCHAR(20) DEFAULT A区 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_space_no (space_no), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车位表; CREATE TABLE parking_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 订单ID, order_no VARCHAR(32) NOT NULL COMMENT 业务订单号, space_id BIGINT NOT NULL COMMENT 车位ID, plate_no VARCHAR(20) NOT NULL COMMENT 车牌号, entry_time DATETIME NOT NULL COMMENT 入场时间, exit_time DATETIME NULL COMMENT 出场时间, fee_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 应收金额, pay_status TINYINT NOT NULL DEFAULT 0 COMMENT 0未支付 1已支付 2已退款, UNIQUE KEY uk_order_no (order_no), KEY idx_plate_entry (plate_no, entry_time), KEY idx_entry_time (entry_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT停车订单表;这段建表 SQL 里有几个值得注意的参数设计。entry_time允许为空不行任何订单都必然有入场时间所以用 NOT NULLexit_time在入场时确实未知所以允许为 NULL这符合「订单创建在前、结算发生在后」的业务顺序。索引上idx_plate_entry覆盖了「按车牌查历史记录」的常见查询场景idx_entry_time则服务运营侧「查某个时段入场了多少车」的统计需求。订单表里的pay_status单独独立出来不跟车位的status耦合。车位状态是物理状态订单支付状态是资金状态混在一个枚举里会导致「车走了但没付钱」时不知道该把车位标记成什么。2.2 设计 status 枚举与时间字段避免事后改表MySQL 里修改表结构本身不复杂ALTER TABLE 可以加列、改类型但如果业务已经上线、表里积累了几十万条订单再回头加状态字段代价就完全不同了。所以建表之前要把状态枚举定义清楚。停车管理系统的订单状态通常不会超过五个进行中还没出场、待支付已出场未付款、已支付、已退款、已作废。车位状态更简单空闲、占用最多加一个预约。把这些状态用整数存代码里用常量类或者枚举类做映射不要直接散落魔法数字。时间字段的设计同样容易埋坑。entry_time和exit_time都用 DATETIME但要注意跨时区部署的问题。如果将来部署到云上、数据库和服务器不在同一时区建议统一用DATETIME存本地时间项目里全局配置一个时区变量展示层再转换。MySQL 的CURRENT_TIMESTAMP默认值只用在一处就够了其余时间字段交给应用层写入避免数据库时钟漂移导致计费偏差。如果确实需要调整已有表尽量用一条 ALTER 语句完成多个变更减少锁表时间ALTER TABLE parking_order ADD COLUMN discount_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 优惠金额, ADD COLUMN coupon_id BIGINT NULL COMMENT 优惠券ID, MODIFY COLUMN fee_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 实收金额原价-优惠, ADD KEY idx_pay_status (pay_status);这条语句把「加列」「改列注释」「加索引」合并成一次执行。注意MODIFY COLUMN fee_amount会重建该列如果表数据量大执行时间会比较长建议在业务低峰期操作并先在测试库跑一遍确认耗时。2.3 索引怎么加订单表按入场时间车位表按状态很多 MySQL 慢查询不是 SQL 写错而是索引没跟上。停车场管理系统里最高频的查询是这几个查询某车位当前是否空闲、查询某车牌最近的入场记录、统计某段时间内的收入。对应到索引就是前面建表时的idx_status和idx_plate_entry。这里有个容易忽略的点idx_plate_entry (plate_no, entry_time)是联合索引查询时如果只按entry_time过滤这个索引用不上只能走idx_entry_time。所以建索引前要梳理真实查询条件把等值查询的字段放前面、范围查询放后面这是 MySQL 索引设计的基本原则。不要为了「查询快」而给每个字段单独建索引。索引会拖慢 INSERT 和 UPDATE停车场系统写多读多索引过多会让入场、出场时的写入明显变慢。一般原则是单表索引控制在五个以内高频查询路径各覆盖一组联合索引即可。3. 搭建 SSM 工程从依赖到事务一条链路配通3.1 工程结构与依赖声明SSM 项目的标准结构分三层Controller 接收请求、Service 处理业务、Mapper 访问数据库。模型层用 POJO 对应表结构VO 对外返回视图数据。工程初始化时Maven 的 pom.xml 里需要声明 Spring、SpringMVC、MyBatis、MySQL 驱动和连接池这几组依赖。下面是一份最小可运行的依赖清单版本号只写主版本实际使用时可根据仓库中最新稳定版调整dependencies !-- Spring 核心与 MVC -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.3.x/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.x/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version5.3.x/version /dependency !-- MyBatis 整合 Spring 的桥接包 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.x/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.1.x/version /dependency !-- MySQL 驱动与连接池 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.x/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.x/version /dependency /dependencies连接池选 Druid 是常见做法它在监控和 SQL 防注入上有现成支持开发阶段可以打开 Druid 的 Web 监控页直接看慢查询和活跃连接数。MyBatis 和 Spring 整合必须引入mybatis-spring这个桥接包缺了它SqlSessionFactory无法注入 Spring 容器。MySQL 驱动用 8.x 时要注意驱动类名是com.mysql.cj.jdbc.Driver不是 5.x 时代的com.mysql.jdbc.Driver。3.2 数据源、事务与 MyBatis 的配置入口SSM 整合时Spring 的配置文件是核心。数据源、事务管理器、SqlSessionFactory 三者的配置顺序有讲究先配数据源再让事务管理器引用它最后把数据源交给 SqlSessionFactory 生成 MyBatis 的会话。以下是一份简化的 Spring 配置context:component-scan base-packagecom.parking/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/parking_db?useUnicodetrueamp;characterEncodingutf8amp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword valueyour_password/ property nameinitialSize value5/ property namemaxActive value20/ /bean bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean tx:annotation-driven transaction-managertransactionManager/JDBC URL 里的serverTimezoneAsia/Shanghai是 MySQL 8 连接时最容易踩的坑漏了它会报时区相关的异常。useUnicodetruecharacterEncodingutf8保证中文车牌和用户名不乱码注意 XML 里 符号要写成amp;。mapperLocations指定了 MyBatis 的 XML 文件目录所有 SQL 映射文件放在 classpath 下的 mapper 包里后续新增 Mapper 不需要再改配置。事务管理用的是 Spring 的DataSourceTransactionManager配合Transactional注解在 Service 方法上声明事务边界。入场操作涉及「插入订单 更新车位状态」两步必须包在同一个事务里出场操作涉及「更新订单 计算费用 更新车位状态」同样要保证原子性。3.3 MyBatis 的 SQL 与参数绑定MyBatis 的 XML 文件推荐把动态 SQL 写在 mapper XML 里Java 接口只声明方法签名。入场的核心 SQL 需要同时完成订单插入和车位状态更新前者用insert语句后者用带条件判断的updateinsert idinsertOrder parameterTypecom.parking.entity.ParkingOrder INSERT INTO parking_order (order_no, space_id, plate_no, entry_time) VALUES (#{orderNo}, #{spaceId}, #{plateNo}, #{entryTime}) /insert update idoccupySpace UPDATE parking_space SET status 1 WHERE id #{spaceId} AND status 0 /updateoccupySpace里AND status 0这个条件非常关键。它保证只有当车位确实是空闲状态时才能被占用如果两个请求同时抢同一个车位数据库的行锁会让后到的那个更新影响行数为 0应用层据此判断「车位已被抢走」返回友好提示。这种带业务条件更新的写法比先 SELECT 再 UPDATE 更抗并发少一次查询还更安全。动态查询按车牌查订单时车牌参数可能为空用if处理select idlistOrders resultTypecom.parking.entity.ParkingOrder SELECT * FROM parking_order where if testplateNo ! null and plateNo ! plate_no #{plateNo} /if if teststartTime ! null AND entry_time gt; #{startTime} /if /where ORDER BY entry_time DESC LIMIT #{offset}, #{pageSize} /selectwhere标签会自动去掉第一个多余的 AND这是 MyBatis 动态 SQL 里最实用的细节。ORDER BY entry_time DESC让最近入场的记录排在最前配合 LIMIT 做分页。MySQL 分页的深坑是偏移量越大越慢offset到几十万时即使有索引也会扫描大量数据后文排查部分会讲具体解法。4. 计费状态机与并发结算让账目和车位状态一致4.1 用 MySQL 存储过程处理出场结算出场结算的逻辑是根据入场时间和当前时间计算停车时长再套用计费规则得到费用最后更新订单状态和车位状态。这套逻辑放 Java 里写也完全可行但放在 MySQL 存储过程里有一个好处——如果后续有多个入口比如岗亭 PC、自助缴费机、小程序都要调用同一个结算规则只需要改数据库里的存储过程不用重新发布应用。下面是一个面向「小时计费 单日封顶」规则的存储过程骨架DELIMITER $$ CREATE PROCEDURE sp_settle_order( IN p_order_id BIGINT, IN p_exit_time DATETIME, OUT p_fee DECIMAL(10,2), OUT p_result TINYINT ) BEGIN DECLARE v_entry_time DATETIME; DECLARE v_hours INT; DECLARE v_space_id BIGINT; DECLARE v_base_fee DECIMAL(10,2) DEFAULT 5.00; DECLARE v_per_hour DECIMAL(10,2) DEFAULT 2.00; DECLARE v_daily_cap DECIMAL(10,2) DEFAULT 30.00; -- 读取订单信息FOR UPDATE 加行锁防止并发结算 SELECT entry_time, space_id INTO v_entry_time, v_space_id FROM parking_order WHERE id p_order_id AND exit_time IS NULL FOR UPDATE; IF v_entry_time IS NULL THEN SET p_fee 0; SET p_result 0; -- 订单状态不对可能已结算 ELSE SET v_hours TIMESTAMPDIFF(HOUR, v_entry_time, p_exit_time); IF v_hours 1 THEN SET v_hours 1; END IF; SET p_fee v_base_fee v_per_hour * (v_hours - 1); -- 超过单日封顶时按封顶金额收取 IF p_fee v_daily_cap THEN SET p_fee v_daily_cap; END IF; UPDATE parking_order SET exit_time p_exit_time, fee_amount p_fee WHERE id p_order_id; UPDATE parking_space SET status 0 WHERE id v_space_id; SET p_result 1; END IF; END$$ DELIMITER ;这个存储过程里值得注意的参数和语句有入参p_order_id定位订单、p_exit_time是出场时间出参p_fee返回计算出的费用、p_result返回执行状态。SELECT ... FOR UPDATE在当前事务内对订单行加排他锁如果两个请求同时对这个订单调用存储过程后到的事务会阻塞等待这是防止重复结算最直接的手段。TIMESTAMPDIFF(HOUR, ...)计算小时差不足一小时按一小时计头部条件DECLARE变量统一管理费率后续调价只需改存储过程里的常量。调用存储过程的方式很简单应用层通过 MyBatis 的Select注解或者 XML 里的select语句调CALL sp_settle_order(...)。注意p_fee这种输出参数需要通过 MyBatis 的Map接收返回值。4.2 并发场景下的订单幂等与状态保护存储过程解决了单订单并发结算的问题但停车场还有其他并发场景同一辆车在出口重复刷两次、扫码枪或道闸误触发送重复出场请求。这类请求本质上是对同一订单的重复结算需要在业务上做幂等保护。常见做法是给订单表加一个settle_version版本号字段每次出场结算时带上条件WHERE id ? AND settle_version ?更新成功后SET settle_version settle_version 1。第二次请求再来时版本号不匹配影响行数为 0直接返回「该订单已结算」。这种做法比查一次状态再更新少一轮数据库交互。除了版本号应用层在 Redis 里用订单号作为 key 做个短时锁也能达到同样效果但 MySQL 行锁的方式不依赖额外组件部署更简单。车位的并发保护则用 3.3 节的AND status 0条件更新。注意这里有一个容易忽略的细节occupySpace更新成功但订单插入失败时事务回滚会把两条 SQL 都撤销车位状态不会停留在占用。前提是这两个操作必须在同一个事务方法里且事务管理器配置正确。MySQL 的隔离级别默认是 REPEATABLE READ处理上述场景足够了。不要把隔离级别调到 SERIALIZABLE那会让停车场的并发吞吐明显下降在高峰期出场排队时每条结算都要等前一条完全提交才能继续体验会变得很差。普通的行锁和条件更新已经解决了核心竞争问题。5. 上线前验证与慢查询排查技巧5.1 用 Docker 拉起一个测试环境验证 DDL 和存储过程开发机上装 MySQL 的方式有很多用 Docker 起一个实例最省事不污染本机环境也不用为版本切换发愁。下面的命令启动一个带端口映射和数据卷的 MySQL 容器docker run -d \ --name parking-mysql \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot123456 \ -e MYSQL_DATABASEparking_db \ -v /opt/mysql-data:/var/lib/mysql \ mysql:8.0-e MYSQL_DATABASEparking_db会在首次启动时自动创建数据库省掉手动建库的步骤。-v挂载数据目录容器删了数据还在。启动后用docker exec -it parking-mysql mysql -uroot -p进入客户端把建表语句和存储过程脚本按顺序执行一遍确认没有语法错误再开始写应用代码。验证环境搭好后有一个值得做的小实验把 2.3 节提到的索引临时删掉然后执行按车牌查订单的 SQL看它的执行时间。这个实验能直观感受索引带来的数量级差异也能帮你在写接口前就发现缺失索引的问题。5.2 EXPLAIN 验证订单表索引是否真正命中排查慢查询时第一步不是看代码而是用 EXPLAIN 看 SQL 的执行计划。以「查某个时间段所有出场未支付的订单」为例EXPLAIN SELECT * FROM parking_order WHERE pay_status 0 AND exit_time IS NOT NULL AND entry_time BETWEEN 2025-01-01 00:00:00 AND 2025-01-31 23:59:59 ORDER BY entry_time DESC LIMIT 20;执行计划里重点看三个字段type达到range或ref说明用上了索引const是最好情况key显示实际命中的索引名rows是预估扫描行数这个数字越大越可疑。如果type是ALL或者key为 NULL说明在走全表扫描。上面这条 SQL 的优化思路是让pay_status的等值条件优先走索引然后通过idx_entry_time做范围过滤。如果订单量很大ORDER BY entry_time DESC LIMIT 20这种深分页查询会越来越慢常见改法是改成「基于游标」的分页——客户端传上一页最后一条记录的entry_time作为查询条件而不是靠大偏移量硬跳。5.3 计费金额自检脚本把边界情况全部跑一遍停车场计费规则最容易出错的不是正常停车而是边界场景停车不足一小时、跨天超过封顶金额、凌晨入场清晨出场、免费时长恰好卡在临界点。上线前把这几类数据直接插进测试库跑一遍结算存储过程对比预期金额和实际金额。下面是一组常见的边界用例-- 用例1停车 20 分钟不足 1 小时按 1 小时收费 INSERT INTO parking_order (order_no, space_id, plate_no, entry_time, exit_time) VALUES (TEST001, 1, 测A0001, 2025-06-01 09:00:00, 2025-06-01 09:20:00); -- 用例2停车 5 小时计算基础费用和超出部分 INSERT INTO parking_order (order_no, space_id, plate_no, entry_time, exit_time) VALUES (TEST002, 2, 测A0002, 2025-06-01 08:00:00, 2025-06-01 13:00:00); -- 用例3停车 26 小时触发单日封顶 INSERT INTO parking_order (order_no, space_id, plate_no, entry_time, exit_time) VALUES (TEST003, 3, 测A0003, 2025-06-01 08:00:00, 2025-06-02 10:00:00);插入后用CALL sp_settle_order(...)逐条结算然后对比fee_amount是否符合计费规则。这类自检脚本要保留下来每次改动存储过程或费率常量后重新执行比人工拿计算器核对快得多。最后提一个运维侧的细节MySQL 的general_log在排查「谁改了数据」时很有用平时保持关闭排查问题时再打开避免日志文件快速膨胀。对停车场这种支付相关系统更重要的是定期备份订单表至少做到每日全量加 binlog这样即使出现错误结算也能从备份里追溯原始数据。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →