ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SSM智能停车系统开发实战:从环境搭建到部署排错

SSM智能停车系统开发实战:从环境搭建到部署排错 简介这是一份基于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批量更新车位或者DATETIME与TIMESTAMP在计费时的精度陷阱。2. SSM 骨架怎么搭用 Maven 先做通一个最小可运行单体2.1 为什么不用 IDEA 自带模板而是从 pom.xml 起步很多“ssm项目”的 zip 解压以后带着.idea和.classpath换一台机器直接废掉。常见做法是忽略那些 IDE 配置自己新建一个 Maven 项目再把源码包里的src/main/java和resources拷进来。这么做的好处是你能清楚看到依赖树而不是等 Tomcat 启动时被不兼容的 jar 包追着跑。我一般会先建一个空白的pom.xml逐步添加依赖。SSM 的核心依赖就那么几个组件作用常见版本建议SpringIOC 容器 事务管理5.3.xSpringMVCController 层请求分发与 Spring 同版本MyBatisSQL 映射与数据库连接3.5.xMyBatis-Spring整合 MyBatis 到 Spring2.0.xMySQL Connector/JJDBC 驱动8.0.xDruid 或 HikariCP数据库连接池1.2.xJacksonJSON 序列化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.version5.3.29/spring.version mybatis.version3.5.13/mybatis.version /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.7/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency /dependenciesspring-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-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mappingload-on-startup必须写否则第一次请求才初始化 SpringMVC首次访问会等 5 秒以上影响演示体验。3. 数据库与持久层车位、订单、用户三张表决定系统上限3.1 数据模型设计车位状态需要单独一张表吗“智能停车系统”里最常见的表结构是用户表t_user、车位表t_parking、订单表t_order。很多人把车位状态直接写成status字段放在车位表里可是车位一旦被预订状态就变成“锁定”这时候如果没有订单号关联后面支付回调时很难追查是哪笔业务锁了车位。我的设计是车位移除status的主状态另外增加lock_status与current_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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里lock_order_id让车位锁定和订单表产生外键逻辑但在代码里不建物理外键因为 MyBatis 做关联查询时物理外键只会增加麻烦。price_hour放在车位表而不是订单表是为了支持不同区域不同计费比如地下一层比地上贵 2 元。3.2 MyBatis 映射写一个带动态 SQL 的 DAO持久层要扛住两个高频操作查询可用车位、更新车位状态。这里直接给出ParkingMapper.xml里最关键的一段。update idlockParking UPDATE t_parking SET status 2, lock_order_id #{orderId} WHERE id #{parkingId} AND status 0 /updateAND status 0不是可有可无这是乐观锁的 SQL 版本。如果两个请求同时抢同一个车位只有一个 update 能匹配到status 0另一个的返回值是 0Service 层拿这个返回值做冲突判断。查询空闲车位用动态 SQL 拼接条件因为前端可能按区域筛也可能按时长筛。select idselectAvailable resultTypecom.example.entity.Parking SELECT * FROM t_parking WHERE status 0 if testlocation ! null and location ! AND location LIKE CONCAT(%, #{location}, %) /if if teststartTime ! 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行级锁保证同一时刻只有一个线程能读到status0的车位。加上lockParking里的条件更新双保险。这里rollbackFor Exception.class不能省略因为 Spring 默认只回滚RuntimeException如果抛的是自定义受检异常事务照样提交。4. 业务接口与页面联动停车计费、预约、结算的完整链路4.1 Controller 设计用 JSON 和页面各取所需SSM 项目常见做法是 Controller 同时返回ModelAndView和ResponseBodyJSON。对于数据操作接口统一返回一个ResultT包装避免前端拿到乱七八糟的字段。RestController RequestMapping(/api/parking) public class ParkingController { Autowired private ParkingService parkingService; GetMapping(/available) public ResultListParkingVO available(RequestParam(required false) String location) { ListParking list parkingService.queryAvailable(location); return Result.success(list); } PostMapping(/order) public ResultOrder createOrder(RequestBody OrderRequest req) { Order order parkingService.createOrder(req); return Result.success(order); } PostMapping(/settle) public ResultSettleVO 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 idreleaseParking 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 版本选 8Maven 仓库路径最好走阿里云镜像否则下载慢到让你以为卡死。如果 zip 里没有 pom.xml只有src和WEB-INF那就手动新建一个 Maven 项目包名改成跟源码一致然后把源码复制进src/main/java把 JSP 放到src/main/webapp。很多毕设项目的包名带有com.foo.parking直接拷贝没问题。5.2 配置文件排查清单数据库连接、字符集、驱动名一个 zip 项目解压后自动就能跑的概率很低最常见失败原因集中在以下三点。这里给出一份排查顺序表。检查项失败特征修复方式jdbc.properties 里的数据库名Unknown database parkingCREATE DATABASE parking DEFAULT CHARACTER SET utf8mb4MySQL 驱动版本ClassNotFoundException: com.mysql.jdbc.DriverMySQL 8 驱动换成com.mysql.cj.jdbc.Driver时区参数The server time zone value Öйú±ê׼ʱ¼ä is unrecognizedURL 加serverTimezoneAsia/ShanghaiTomcat 版本java.lang.ClassNotFoundException: javax.servlet.jsp.jstl.core.Config在 pom 添加jstl和standard依赖配置文件最常见的坑是jdbc.properties里密码带特殊字符。比如密码为123456在 properties 里不需要转义但如果你用拼接参数必须写成amp;。我的习惯是在jdbc.properties里把整个 URL 写成一行jdbc.urljdbc:mysql://localhost:3306/parking?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse然后用 Spring 加载context:property-placeholder locationclasspath:jdbc.properties/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean5.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 - Artifacts里WEB-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区001到C区010前 20 个车位价格每小时 6 元后面 4 元。LPAD保证排序时 001、010、100 的字典序一致不会出现 2 排在 10 前面的情况。存储过程是一次性操作跑完可以删掉。最后一个实用技巧给t_order增加一个create_time索引EXPLAIN一下就会发现停车记录查询从全表扫描变成索引范围扫描。智能停车系统虽然数据量小但演示时如果某个页面按日期筛选订单慢多半是索引没建。在 SSM 里你不会像 MyBatis-Plus 那样自动建索引所以建表语句里就要写全这也是源码包和手写工程的区别所在。本文还有配套的精品资源点击获取
返回列表