SSM智能停车系统开发实战:从环境搭建到部署排错
2026/9/14 9:53:04 网站建设 项目流程

简介:这是一份基于Spring、SpringMVC、MyBatis三大框架构建的智能停车系统Java Web项目,完整可运行,面向Java学习者、SSM框架初学者及毕业设计开发者,可用于掌握企业级Web开发的完整流程。压缩包共771个文件,约14.08MB,包含163个css、149个png、88个class、83个wxss、74个js、64个java、37个jar、28个xml、26个json等类型,其中Java源码与XML映射文件体现后端业务逻辑,JSP与wxss负责界面展示,SQL脚本初始化数据表,jar包提供第三方依赖支持,目录结构按清晰的功能模块划分,便于学习与二次开发。已有389人学习下载。整体来看,项目中包含车位管理、用户预约、停车记录、微信端接口等核心模块,采用MVC分层与前后端分离设计,还可结合物联网思路实现实时监控车位状态。既可作为参考实现,也可直接部署运行,有助于理解SSM整合、MyBatis映射、事务处理与前后端交互的实战技巧。

1. 拿到“ssm项目 智能停车系统.zip”后,先别急着解压

很多人以为一个 SSM 项目的 zip 压缩包只是改改数据库端口就能跑,实际上大多数“智能停车系统”源码包倒在你面前时,第一关不是代码,而是环境。SSM 四件套——Spring、SpringMVC、MyBatis,外加一个 Tomcat,版本之间稍微有点错位,启动日志就能给你刷出十几行ClassNotFoundException。而“智能停车系统”这个题目本身又比普通 CRUD 多了一层业务复杂度:车位状态实时更新、停车时长计费、订单状态流转,这些逻辑如果不在数据库和 MyBatis 映射层设计好,后台页面再漂亮也撑不住演示。

所以这篇文章不打算给一份编译好的“跑通即可”的作业,而是站在一线工程师的角度,把 SSM 项目里最容易翻车的几个剖面切开:先搭一个能手动控制的 SSM 最小骨架,再设计车位与订单的数据库模型,然后把停车计费和预约流程在 Controller 与 Service 之间打通,最后处理从 zip 解压到 Tomcat 部署最常见的几个问题。新手能照着做,老手可以看看你的边界——比如 MyBatis 的<foreach>批量更新车位,或者DATETIMETIMESTAMP在计费时的精度陷阱。

2. SSM 骨架怎么搭:用 Maven 先做通一个最小可运行单体

2.1 为什么不用 IDEA 自带模板,而是从 pom.xml 起步

很多“ssm项目”的 zip 解压以后带着.idea.classpath,换一台机器直接废掉。常见做法是忽略那些 IDE 配置,自己新建一个 Maven 项目,再把源码包里的src/main/javaresources拷进来。这么做的好处是你能清楚看到依赖树,而不是等 Tomcat 启动时被不兼容的 jar 包追着跑。

我一般会先建一个空白的pom.xml,逐步添加依赖。SSM 的核心依赖就那么几个:

组件作用常见版本建议
SpringIOC 容器 + 事务管理5.3.x
SpringMVCController 层请求分发与 Spring 同版本
MyBatisSQL 映射与数据库连接3.5.x
MyBatis-Spring整合 MyBatis 到 Spring2.0.x
MySQL Connector/JJDBC 驱动8.0.x
Druid 或 HikariCP数据库连接池1.2.x
JacksonJSON 序列化2.13.x

这里注意,Spring 6.x 需要 JDK 17+,而大多数毕业设计运行的 Tomcat 8.5 或 9.0 更适合搭配 JDK 8 + Spring 5.3.x。如果 zip 包里的代码是javax.servlet开头,那就别用 Spring Boot,老老实实配 SSM。

<properties> <spring.version>5.3.29</spring.version> <mybatis.version>3.5.13</mybatis.version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.20</version> </dependency> </dependencies>

spring-webmvc传依赖会带来 spring-context 和 spring-beans,所以不需要单独声明。mybatis-spring必须使用与 MyBatis 版本兼容的 2.0.x,如果你在 pom 里写 1.3.3,启动时会报SQLSessionFactoryBean方法缺失。

2.2 web.xml 与 Spring 容器分层

SSM 不是 Spring Boot,必须有两个 Spring 容器:一个由ContextLoaderListener加载,负责 Service 和 Dao;一个由DispatcherServlet加载,负责 Controller。虽然是老生常谈,但确实有大量项目把所有 bean 都塞进 spring-mvc.xml,导致事务配置失效。

有一个容易忽略的配置是url-pattern。如果你想让 REST 风格的/api/parking/order/{id}生效,并且允许静态资源访问,最好把 DispatcherServlet 映射为/,然后在 spring-mvc.xml 里加上<mvc:resources>。如果你图省事映射成*.do,那 jQuery 和 Layui 这些前端资源就得放到 filter 里去放行,多一层麻烦。

<servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>

load-on-startup必须写,否则第一次请求才初始化 SpringMVC,首次访问会等 5 秒以上,影响演示体验。

3. 数据库与持久层:车位、订单、用户三张表决定系统上限

3.1 数据模型设计:车位状态需要单独一张表吗

“智能停车系统”里最常见的表结构是用户表t_user、车位表t_parking、订单表t_order。很多人把车位状态直接写成status字段放在车位表里,可是车位一旦被预订,状态就变成“锁定”,这时候如果没有订单号关联,后面支付回调时很难追查是哪笔业务锁了车位。

我的设计是车位移除status的主状态,另外增加lock_statuscurrent_order_id,这样既能表达“空闲/占用/锁定”的瞬时状态,又能追溯到订单。DBA 看到这种设计可能觉得冗余,但对一个 SSM 项目来说,清晰的业务字段比范式程度更值钱。

CREATE TABLE `t_parking` ( `id` INT NOT NULL AUTO_INCREMENT, `code` VARCHAR(16) NOT NULL COMMENT '车位编号', `location` VARCHAR(64) DEFAULT NULL, `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0空闲 1占用 2锁定', `lock_order_id` INT DEFAULT NULL COMMENT '锁定的订单ID', `price_hour` DECIMAL(10,2) DEFAULT 5.00, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_code` (`code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里lock_order_id让车位锁定和订单表产生外键逻辑,但在代码里不建物理外键,因为 MyBatis 做关联查询时物理外键只会增加麻烦。price_hour放在车位表而不是订单表,是为了支持不同区域不同计费,比如地下一层比地上贵 2 元。

3.2 MyBatis 映射:写一个带动态 SQL 的 DAO

持久层要扛住两个高频操作:查询可用车位、更新车位状态。这里直接给出ParkingMapper.xml里最关键的一段。

<update id="lockParking"> UPDATE t_parking SET status = 2, lock_order_id = #{orderId} WHERE id = #{parkingId} AND status = 0 </update>

AND status = 0不是可有可无,这是乐观锁的 SQL 版本。如果两个请求同时抢同一个车位,只有一个 update 能匹配到status = 0,另一个的返回值是 0,Service 层拿这个返回值做冲突判断。

查询空闲车位用动态 SQL 拼接条件,因为前端可能按区域筛,也可能按时长筛。

<select id="selectAvailable" resultType="com.example.entity.Parking"> SELECT * FROM t_parking WHERE status = 0 <if test="location != null and location != ''"> AND location LIKE CONCAT('%', #{location}, '%') </if> <if test="startTime != null"> AND id NOT IN ( SELECT parking_id FROM t_order WHERE <![CDATA[ end_time > #{startTime} ]]> ) </if> ORDER BY code </select>

注意CDATA包裹大于号,XML 里裸写>没问题,但>=必须转义。这种写法也符合尚硅谷 SSM 笔记里强调的动态 SQL 最佳实践。这个查询的逻辑是:先看状态空闲,再看这个时间点有没有未结束的订单占用,两层过滤才算真正空闲。

3.3 Service 事务边界:计费逻辑不能放在 Controller

很多源码包把计费写在 Controller 里,BigDecimal计算和状态更新挤在一坨,到时候事务不知道该回滚什么。我习惯把一次停车流程的入口放在 Service 层,加上@Transactional

@Override @Transactional(rollbackFor = Exception.class) public Order createOrder(ParkingOrderRequest req) { Parking parking = parkingMapper.selectByIdForUpdate(req.getParkingId()); if (parking == null || parking.getStatus() != 0) { throw new BizException("车位不存在或已被占用"); } // 生成订单号 String orderNo = generateOrderNo(); // 锁定车位 int rows = parkingMapper.lockParking(req.getParkingId(), orderNo); if (rows == 0) { throw new BizException("并发冲突,请重新选择车位"); } Order order = new Order(); order.setOrderNo(orderNo); order.setParkingId(req.getParkingId()); order.setUserActivityId(req.getUserId()); order.setStatus(0); // 0进行中 1已完成 order.setCreateTime(new Date()); orderMapper.insert(order); return order; }

selectByIdForUpdate会在 SQL 里生成FOR UPDATE,行级锁保证同一时刻只有一个线程能读到status=0的车位。加上lockParking里的条件更新,双保险。这里rollbackFor = Exception.class不能省略,因为 Spring 默认只回滚RuntimeException,如果抛的是自定义受检异常,事务照样提交。

4. 业务接口与页面联动:停车计费、预约、结算的完整链路

4.1 Controller 设计:用 JSON 和页面各取所需

SSM 项目常见做法是 Controller 同时返回ModelAndView@ResponseBodyJSON。对于数据操作接口,统一返回一个Result<T>包装,避免前端拿到乱七八糟的字段。

@RestController @RequestMapping("/api/parking") public class ParkingController { @Autowired private ParkingService parkingService; @GetMapping("/available") public Result<List<ParkingVO>> available(@RequestParam(required = false) String location) { List<Parking> list = parkingService.queryAvailable(location); return Result.success(list); } @PostMapping("/order") public Result<Order> createOrder(@RequestBody OrderRequest req) { Order order = parkingService.createOrder(req); return Result.success(order); } @PostMapping("/settle") public Result<SettleVO> settle(@RequestBody SettleRequest req) { SettleVO vo = parkingService.settle(req.getOrderNo()); return Result.success(vo); } }

@RestController在 Spring 4.3 以后推荐使用,省去每个方法加@ResponseBody。前端如果走 AJAX,返回 JSON;如果走表单提交,需要另外写一个ModelAndView的方法。页面跳转和接口分离是“智能停车系统”这种毕设正确的组织方式,否则后期加一个微信小程序端时没法复用。

4.2 停车计费:跨天和免费时长的精确计算

计费是智能停车系统的核心展示点,也是面试时容易被追问的细节。设计计费规则:首小时 6 元,之后每小时 4 元,不足 1 小时按 1 小时算,15 分钟内免费,跨天累加。

public static BigDecimal calcFee(Date start, Date end, BigDecimal firstHourPrice, BigDecimal perHourPrice) { long minutes = (end.getTime() - start.getTime()) / (1000 * 60); if (minutes <= 15) { return BigDecimal.ZERO; } // 向上取整到小时 long hours = (minutes + 59) / 60; BigDecimal total = perHourPrice.multiply(BigDecimal.valueOf(hours)); // 不足一小时按一小时算后,首小时不再额外加,只是单价不同 return total; }

这里隐藏一个精度问题:不能拿double计算金额,必须BigDecimal。另外,跨天时getTime()差值已经算出了总分钟数,不需要判断日期边界,这也是为什么end_time必须存DATETIME,不要存DATE,否则跨天订单直接失效。

针对“同一车位不同时段价格不同”这种复杂需求,可以在t_order里加一个fee_snapshot字段,在创建订单时把单价快照存进去。这样后续修改车位单价,历史订单结算不受影响,这也是我在做过几个停车系统后总结出的经验:费用计算永远以订单快照为准,而不是实时读车位价格。

4.3 结算链路与状态机

结算时不能只更新订单状态,还要同时释放车位。这两步必须在一个事务里。如果你在 Service 调用了两个 Mapper,每个 Mapper 的 update 各自独立提交,那中途一旦失败,会出现订单已完成但车位依然是占用状态。

@Transactional public SettleVO settle(String orderNo) { Order order = orderMapper.selectByOrderNo(orderNo); if (order == null || order.getStatus() != 0) { throw new BizException("订单不存在或已结算"); } BigDecimal fee = calcFee(order.getCreateTime(), new Date(), parkingMapper.selectPriceByParkingId(order.getParkingId())); // 更新订单 order.setStatus(1); order.setSettleTime(new Date()); order.setFee(fee); orderMapper.updateStatus(order); // 释放车位 parkingMapper.releaseParking(order.getParkingId()); // 返回结果 SettleVO vo = new SettleVO(); vo.setOrderNo(orderNo); vo.setFee(fee); return vo; }

释放车位的 SQL 这样写:

<update id="releaseParking"> UPDATE t_parking SET status = 0, lock_order_id = NULL WHERE id = #{parkingId} </update>

如果中间计算fee出异常,事务回滚,订单还是进行中,车位还锁定着,不会产生死账。这个状态机只用了两个状态:0 进行中、1 已完成。实际系统可以再加一个 2 已取消,但取消订单要用定时任务去扫超时未支付订单,在 SSM 里可以用@Scheduled,需要在 spring-mvc.xml 或 applicationContext.xml 开启<task:annotation-driven />

5. 从 zip 到可演示:导入项目、改配置、部署 Tomcat 的排错要点

5.1 拿到源码包后的第一件事:看是 Maven 还是普通 Web 项目

zip 里有pom.xml说明是 Maven 工程,没有的话你就是拿到了一个已经编译过的.class或者原始 Web 工程。两种情况处理方式完全不同。有 pom 的时候,在 IDEA 里File -> Open -> 选择 pom.xml让 Maven 自动下载依赖。注意不要让 IDEA 自动导入时选择“信任项目”以外的选项,JDK 版本选 8,Maven 仓库路径最好走阿里云镜像,否则下载慢到让你以为卡死。

如果 zip 里没有 pom.xml,只有srcWEB-INF,那就手动新建一个 Maven 项目,包名改成跟源码一致,然后把源码复制进src/main/java,把 JSP 放到src/main/webapp。很多毕设项目的包名带有com.foo.parking,直接拷贝没问题。

5.2 配置文件排查清单:数据库连接、字符集、驱动名

一个 zip 项目解压后自动就能跑的概率很低,最常见失败原因集中在以下三点。这里给出一份排查顺序表。

检查项失败特征修复方式
jdbc.properties 里的数据库名Unknown database 'parking'CREATE DATABASE parking DEFAULT CHARACTER SET utf8mb4
MySQL 驱动版本ClassNotFoundException: com.mysql.jdbc.DriverMySQL 8 驱动换成com.mysql.cj.jdbc.Driver
时区参数The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognizedURL 加serverTimezone=Asia/Shanghai
Tomcat 版本java.lang.ClassNotFoundException: javax.servlet.jsp.jstl.core.Config在 pom 添加jstlstandard依赖

配置文件最常见的坑是jdbc.properties里密码带特殊字符。比如密码为123@456,在 properties 里不需要转义,但如果你用&拼接参数,必须写成&amp;。我的习惯是在jdbc.properties里把整个 URL 写成一行:

jdbc.url=jdbc:mysql://localhost:3306/parking?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false

然后用 Spring 加载:

<context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean>

5.3 部署到 Tomcat 的 clean 与 artifact 配置

IDEA 里部署 war 包时,如果出现SEVERE: The required Server component failed to start so Tomcat is unable to start,多半是端口占用或者 artfact 没有配置。在 Run Configurations 里的 Deploy 标签页,选中war exploded而不是war,这样热部署只更新变动文件,演示时改个小样式不用重启。

还有一个很隐蔽的问题:src/main/resources里的 XML 如果没有打包到classes目录,启动时会报org.springframework.beans.factory.BeanDefinitionStoreException。检查Project Structure -> ArtifactsWEB-INF/classes是否包含resources目录下的所有 xml。Maven 默认会把 resources 放进去,但如果你手动改过 Settings,可能漏。最快的验证方式:启动报错看完整堆栈,一旦出现class path resource [spring-mvc.xml] cannot be opened,不要看别的,先看 classes 目录里有没有这个文件。

5.4 演示前做一次假数据注入

毕业设计答辩或项目演示最尴尬的页面是空白表格。给项目内置一个data.sql或者init.sql,在第一次启动时把车位数据导进去。可以用 Spring 的jdbc初始化脚本,但更简单的是手动在 Navicat 里执行。我这里给出一个生成 50 个车位编号的 SQL 技巧,用存储过程一次插入。

DROP PROCEDURE IF EXISTS init_parking; DELIMITER $$ CREATE PROCEDURE init_parking() BEGIN DECLARE i INT DEFAULT 1; DECLARE area VARCHAR(16); WHILE i <= 50 DO SET area = CASE WHEN i <= 20 THEN 'A区' WHEN i <= 40 THEN 'B区' ELSE 'C区' END; INSERT INTO t_parking (code, location, status, price_hour) VALUES (CONCAT(area, LPAD(i, 3, '0')), area, 0, CASE WHEN i <= 20 THEN 6.00 ELSE 4.00 END); SET i = i + 1; END WHILE; END$$ DELIMITER ; CALL init_parking();

执行后车位编号为A区001C区010,前 20 个车位价格每小时 6 元,后面 4 元。LPAD保证排序时 001、010、100 的字典序一致,不会出现 2 排在 10 前面的情况。存储过程是一次性操作,跑完可以删掉。

最后一个实用技巧:给t_order增加一个create_time索引,EXPLAIN一下就会发现停车记录查询从全表扫描变成索引范围扫描。智能停车系统虽然数据量小,但演示时如果某个页面按日期筛选订单慢,多半是索引没建。在 SSM 里你不会像 MyBatis-Plus 那样自动建索引,所以建表语句里就要写全,这也是源码包和手写工程的区别所在。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询