简介:本资源是一套基于SSM(Spring+SpringMVC+MyBatis)框架开发的车辆维修管理系统,专为计算机专业本科毕业设计(bishe)学生及Java进阶学习者打造,覆盖用户管理、车辆档案、故障登记、维修订单、零配件购销与使用、访客统计等核心业务模块,可直接部署运行并作为毕设课题提交。压缩包共184个文件,含38个Java源码(如OrderInfoController、CarInfoServiceImpl等)、24个XML配置与映射文件、21个HTML页面、12个PNG图标资源及9个CSS样式文件,辅以JAR依赖、SQL建库脚本和Tomcat兼容的class编译文件,整体7.58MB,结构完整、开箱即用。已有260人下载学习,所有模块均经严格调试,配套MySQL 5.0+数据库脚本与Navicat可视化支持,适配Eclipse/Idea开发环境,便于理解SSM分层架构、前后端交互逻辑与企业级维修业务建模方法。
1. 为什么一个“车辆维修管理系统”非得用 SSM 框架来落地?
你手头有个汽修厂的数字化需求:要记录每台进厂车辆的故障描述、维修工单、配件领用、技师派工、结算金额,还要能按车牌号查历史维修记录、按月份统计维修收入、导出 Excel 报表。这不是 ERP 级别系统,但也不是 Excel 表格能扛得住的——数据要关联(车辆 ↔ 工单 ↔ 配件 ↔ 技师)、操作要并发(前台接车、车间派工、财务开票可能同时发生)、界面要可维护(老板想改个字段名不能找程序员改三天)。这时候,SSM(Spring + Spring MVC + MyBatis)不是“选它”,而是“绕不开”:它把 Java Web 开发里最硬的三块骨头——事务控制(修车钱不能漏记)、请求路由(不同页面走不同处理逻辑)、SQL 映射(一张工单关联五张表怎么查才不慢)——用成熟、低侵入、易调试的方式焊死在一块。尤其对中小汽修企业或高校课程设计而言,SSM 的 XML 配置可读性强、注解驱动清晰、MyBatis 手写 SQL 能精准控制查询性能,比 Spring Boot 自动装配更利于理解数据流向。本文就从零开始,带你用 SSM 搭建一个真正能跑起来、能改字段、能加报表、能上线的车辆维修管理系统。
2. SSM 三层架构怎么搭?从 pom.xml 到 Controller 层的最小闭环
SSM 不是三个框架简单拼凑,而是一套分层协作机制:Spring 管 Bean 和事务,Spring MVC 处理 HTTP 请求与响应,MyBatis 负责数据库 CRUD。搭建第一步不是写代码,而是确认依赖版本兼容性——这是新手踩坑最多的地方。Spring 5.x 与 MyBatis 3.4+ 兼容稳定,但若误引入 Spring 6.x(要求 JDK 17+),则 MyBatis-Spring 2.x 会报NoSuchMethodError。以下为经验证的 Maven 依赖组合(pom.xml 片段):
<!-- Spring 核心 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.30</version> </dependency> <!-- Spring MVC --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.30</version> </dependency> <!-- MyBatis --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.13</version> </dependency> <!-- MyBatis-Spring 整合包(关键!) --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency> <!-- 数据库驱动(以 MySQL 8.0 为例) --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- 连接池(推荐 HikariCP) --> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>5.0.1</version> </dependency>提示:
mybatis-spring版本必须与 Spring 版本严格匹配。Spring 5.3.x 对应 mybatis-spring 2.0.x;若用 Spring 4.3.x,则需降级到 mybatis-spring 1.3.x。版本错配会导致SqlSessionFactoryBean初始化失败,日志中出现No qualifying bean of type 'org.apache.ibatis.session.SqlSessionFactory'。
2.1 Spring 配置:用 XML 控制 Bean 生命周期与事务边界
SSM 中 Spring 的核心配置文件applicationContext.xml负责两件事:声明式事务管理(保证“创建工单+扣减库存”要么全成功,要么全回滚)和 DAO 层 Bean 注册。以下是最简有效配置:
<!-- 扫描 Service 包,自动注册 @Service Bean --> <context:component-scan base-package="com.vehicle.service"/> <!-- 数据源配置(HikariCP) --> <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close"> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/vehicle_repair?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"/> <property name="username" value="root"/> <property name="password" value="123456"/> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="maximumPoolSize" value="20"/> <property name="minimumIdle" value="5"/> </bean> <!-- MyBatis SqlSessionFactory --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <!-- Mapper 接口扫描(替代手动定义每个 Mapper Bean) --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.vehicle.dao"/> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> </bean> <!-- 声明式事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <!-- 启用 @Transactional 注解 --> <tx:annotation-driven transaction-manager="transactionManager"/>这段配置的关键在于:MapperScannerConfigurer自动扫描com.vehicle.dao下所有接口并生成代理对象,省去为每个 DAO 写<bean>标签;DataSourceTransactionManager绑定到dataSource,后续在 Service 方法上加@Transactional即可开启事务。
2.2 Spring MVC 配置:让 /repair/list 这样的 URL 真正映射到 Java 方法
spring-mvc.xml文件专注 HTTP 层:URL 路由、视图解析、静态资源放行。注意两点:一是DispatcherServlet默认只拦截.do或/,需显式配置/*才能处理/repair/add;二是 JSP 视图解析器路径必须与实际WEB-INF/jsp/目录结构一致:
<!-- 扫描 Controller 包 --> <context:component-scan base-package="com.vehicle.controller" use-default-filters="false"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan> <!-- 启用注解驱动(@RequestMapping, @ResponseBody) --> <mvc:annotation-driven/> <!-- 静态资源放行(CSS/JS/IMG) --> <mvc:default-servlet-handler/> <!-- 视图解析器:返回 "list" → /WEB-INF/jsp/list.jsp --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> <!-- 文件上传支持(维修图片、工单附件) --> <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="maxUploadSize" value="10485760"/> <!-- 10MB --> </bean>2.3 MyBatis 配置:手写 SQL 比全自动 ORM 更适合维修业务
车辆维修的核心实体是RepairOrder(维修工单),它必然关联Vehicle(车辆)、Technician(技师)、Part(配件)三张表。MyBatis 的优势在于:你可以用<resultMap>精确控制嵌套查询,避免 N+1 查询问题。例如,查一个工单及其关联的车辆信息,不写两条 SQL,而用一次 JOIN:
<!-- mapper/RepairOrderMapper.xml --> <mapper namespace="com.vehicle.dao.RepairOrderMapper"> <resultMap id="RepairOrderWithVehicle" type="com.vehicle.entity.RepairOrder"> <id property="id" column="ro.id"/> <result property="orderNo" column="ro.order_no"/> <result property="status" column="ro.status"/> <association property="vehicle" javaType="com.vehicle.entity.Vehicle"> <id property="id" column="v.id"/> <result property="plateNumber" column="v.plate_number"/> <result property="brand" column="v.brand"/> </association> </resultMap> <!-- 查询带车辆信息的工单列表 --> <select id="selectListWithVehicle" resultMap="RepairOrderWithVehicle"> SELECT ro.*, v.id AS 'v.id', v.plate_number, v.brand FROM repair_order ro LEFT JOIN vehicle v ON ro.vehicle_id = v.id WHERE ro.status != 'DELETED' ORDER BY ro.create_time DESC LIMIT #{offset}, #{limit} </select> </mapper>这个<association>标签让 MyBatis 在结果集中自动组装RepairOrder.vehicle对象,比 Hibernate 的懒加载更可控——维修系统里,90% 的工单列表页都需要显示车牌号,提前 JOIN 比事后 N 次SELECT * FROM vehicle WHERE id=?效率高得多。
3. 车辆维修核心业务怎么落地?从数据库建模到 Controller 接口实现
一个能用的车辆维修系统,必须覆盖“接车登记 → 派工 → 维修执行 → 结算开票”主流程。我们以“新增维修工单”为切口,展示 SSM 如何串联三层。
3.1 数据库设计:用外键约束保障业务一致性
维修工单表repair_order必须强制关联车辆、技师、状态字典。MySQL 建表语句如下(含关键约束):
CREATE TABLE `repair_order` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL COMMENT '工单编号,如 RP20240501001', `vehicle_id` BIGINT NOT NULL COMMENT '关联车辆ID', `technician_id` BIGINT NOT NULL COMMENT '指派技师ID', `fault_description` TEXT COMMENT '故障描述', `status` VARCHAR(20) NOT NULL DEFAULT 'WAITING' COMMENT '状态:WAITING/IN_PROGRESS/DONE/PAYED', `total_amount` DECIMAL(10,2) DEFAULT 0.00 COMMENT '总金额', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT `fk_vehicle` FOREIGN KEY (`vehicle_id`) REFERENCES `vehicle`(`id`) ON DELETE RESTRICT, CONSTRAINT `fk_technician` FOREIGN KEY (`technician_id`) REFERENCES `technician`(`id`) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意:
ON DELETE RESTRICT是关键。当某辆车被删除时,若存在未完成的工单,数据库直接拒绝删除,避免产生“孤儿工单”。这比在 Service 层做校验更可靠——因为多线程下,校验与插入之间存在时间窗口。
3.2 Service 层:用 @Transactional 保证“创建工单+更新车辆状态”原子性
维修工单创建不是简单插入一条记录,还需同步更新车辆的last_repair_time字段。这两步必须在一个事务内完成:
@Service public class RepairOrderService { @Autowired private RepairOrderMapper repairOrderMapper; @Autowired private VehicleMapper vehicleMapper; @Transactional(rollbackFor = Exception.class) public void createRepairOrder(RepairOrder order) { // 1. 插入工单 repairOrderMapper.insert(order); // 2. 更新车辆最后维修时间(关键:同一事务内) Vehicle vehicle = new Vehicle(); vehicle.setId(order.getVehicleId()); vehicle.setLastRepairTime(new Date()); vehicleMapper.updateById(vehicle); // 若此处抛异常(如网络中断),上面 insert 也会回滚 } }@Transactional注解生效的前提是:该方法由 Spring 容器管理的 Bean 调用(即通过@Autowired注入的 service 对象调用),而非this.createRepairOrder()直接调用——后者会绕过代理,事务失效。
3.3 Controller 层:接收 JSON 参数并返回标准响应体
前端通常用 AJAX 提交工单数据,Controller 需接收 JSON 并返回统一格式:
@Controller @RequestMapping("/repair") public class RepairOrderController { @Autowired private RepairOrderService repairOrderService; /** * 新增维修工单 * POST /repair/add * RequestBody: {"vehicleId":101,"technicianId":201,"faultDescription":"发动机异响"} */ @ResponseBody @PostMapping("/add") public Result add(@RequestBody RepairOrder order) { try { // 生成工单编号:RP + 年月日 + 3位流水号 String orderNo = generateOrderNo(); order.setOrderNo(orderNo); order.setStatus("WAITING"); repairOrderService.createRepairOrder(order); return Result.success("工单创建成功,编号:" + orderNo); } catch (Exception e) { return Result.error("创建失败:" + e.getMessage()); } } private String generateOrderNo() { String dateStr = new SimpleDateFormat("yyyyMMdd").format(new Date()); // 实际项目中应使用 Redis 或数据库自增保证唯一性 int seq = (int)(Math.random() * 900) + 100; return "RP" + dateStr + seq; } }@ResponseBody确保返回 JSON 而非跳转 JSP 页面;@RequestBody将前端 JSON 自动反序列化为RepairOrder对象;Result是自定义响应类,包含code、msg、data三字段,便于前端统一处理。
3.4 JSP 页面:用 JSTL 渲染工单列表并支持分页
/WEB-INF/jsp/repair/list.jsp使用 JSTL 标签遍历工单列表,并集成分页控件:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head><title>维修工单列表</title></head> <body> <h2>维修工单管理</h2> <table border="1"> <tr> <th>工单号</th> <th>车牌号</th> <th>故障描述</th> <th>状态</th> <th>创建时间</th> <th>操作</th> </tr> <c:forEach items="${page.list}" var="order"> <tr> <td>${order.orderNo}</td> <td>${order.vehicle.plateNumber}</td> <td>${order.faultDescription}</td> <td> <c:choose> <c:when test="${order.status == 'WAITING'}">待派工</c:when> <c:when test="${order.status == 'IN_PROGRESS'}">维修中</c:when> <c:otherwise>已完成</c:otherwise> </c:choose> </td> <td>${order.createTime}</td> <td> <a href="/repair/detail?id=${order.id}">详情</a> <a href="/repair/edit?id=${order.id}">编辑</a> </td> </tr> </c:forEach> </table> <!-- 分页导航 --> <div class="pagination"> <c:if test="${page.pageNum > 1}"> <a href="/repair/list?pageNum=${page.pageNum - 1}">上一页</a> </c:if> <span>第 ${page.pageNum} 页,共 ${page.pages} 页</span> <c:if test="${page.pageNum < page.pages}"> <a href="/repair/list?pageNum=${page.pageNum + 1}">下一页</a> </c:if> </div> </body> </html>这里${page.list}来自 Controller 返回的ModelAndView,其中page是 PageHelper 插件封装的分页对象。PageHelper 是 SSM 生态中最常用的分页工具,只需在 Service 方法前加一行PageHelper.startPage(pageNum, pageSize),后续的mapper.selectList()就自动变成分页查询。
4. SSM 车辆维修系统怎么调优?3 个必改参数与 2 类典型慢查询
SSM 项目上线后,最常见的性能瓶颈不在框架本身,而在数据库访问层。以下是针对车辆维修场景的实测优化点。
4.1 MyBatis 一级缓存陷阱:为什么修改车辆信息后工单页面没刷新?
MyBatis 默认开启一级缓存(SqlSession 级别),同一个 SqlSession 内重复查询相同 SQL 会直接返回缓存结果。这在维修系统中极易引发数据不一致:比如管理员在后台修改了某辆车的品牌,但用户刷新工单列表页时,因复用旧 SqlSession,仍显示旧品牌。
解决方案:在需要实时性的查询上禁用一级缓存
<!-- mapper/RepairOrderMapper.xml --> <select id="selectListWithVehicle" resultMap="RepairOrderWithVehicle" flushCache="true"> SELECT ro.*, v.id AS 'v.id', v.plate_number, v.brand FROM repair_order ro LEFT JOIN vehicle v ON ro.vehicle_id = v.id WHERE ro.status != 'DELETED' ORDER BY ro.create_time DESC </select>flushCache="true"表示每次执行该 SQL 前清空本地缓存,强制查询数据库。注意:此属性仅对SELECT语句有效,且只影响当前 Mapper 的缓存,不影响其他 Mapper。
4.2 HikariCP 连接池参数:修车高峰期连接不够用怎么办?
汽修厂下午 2–4 点是接车高峰,若连接池最大连接数maximumPoolSize设置过小(如默认 10),会出现HikariPool-1 - Connection is not available, request timed out after 30000ms错误。
根据并发量调整关键参数:
| 参数 | 推荐值 | 说明 |
|---|---|---|
maximumPoolSize | CPU核数 × 2 + 有效磁盘数 | 一般设为 20–50;车辆维修系统并发读多写少,20 足够支撑 100 用户 |
connectionTimeout | 30000(30秒) | 超时时间不宜过短,避免瞬时高峰误判为故障 |
idleTimeout | 600000(10分钟) | 空闲连接超时,防止数据库端因长时间空闲断连 |
leakDetectionThreshold | 60000(60秒) | 检测连接泄漏,若某连接被借出超 60 秒未归还,打印警告 |
在applicationContext.xml的HikariDataSourceBean 中配置:
<property name="maximumPoolSize" value="30"/> <property name="connectionTimeout" value="30000"/> <property name="idleTimeout" value="600000"/> <property name="leakDetectionThreshold" value="60000"/>4.3 慢查询定位:用 EXPLAIN 分析“按车牌查历史工单”为何卡顿
用户输入车牌号粤B12345查询历史维修记录,响应超 5 秒。执行EXPLAIN SELECT * FROM repair_order WHERE vehicle_id IN (SELECT id FROM vehicle WHERE plate_number = '粤B12345');发现type=ALL(全表扫描)。
根因与修复:
vehicle表缺少plate_number索引;- 子查询导致无法利用索引。
优化后 SQL(改用 JOIN):
SELECT ro.* FROM repair_order ro INNER JOIN vehicle v ON ro.vehicle_id = v.id WHERE v.plate_number = '粤B12345' ORDER BY ro.create_time DESC;并在vehicle表上添加联合索引:
ALTER TABLE vehicle ADD INDEX idx_plate_brand (plate_number, brand);该索引同时支持“按车牌查询”和“按车牌+品牌筛选”两种高频场景,且plate_number作为最左前缀,确保WHERE plate_number = ?能走索引。
5. 怎么快速验证 SSM 车辆维修系统是否跑通?用 curl 命令直击核心接口
不启动 Tomcat、不打开浏览器,用命令行验证系统健康度,是运维和开发联调的黄金习惯。以下 4 条 curl 命令覆盖 SSM 项目最关键的四个环节:
5.1 验证 Spring MVC 路由是否生效
curl -X GET "http://localhost:8080/repair/list?pageNum=1"预期返回:HTTP 200,且响应体包含<table>标签及工单数据。若返回 404,检查web.xml中DispatcherServlet的url-pattern是否为/*;若返回 500,查看 Tomcat 日志中是否有No mapping found for HTTP request。
5.2 验证 MyBatis 数据库连接是否正常
curl -X POST "http://localhost:8080/repair/add" \ -H "Content-Type: application/json" \ -d '{"vehicleId":1,"technicianId":1,"faultDescription":"测试工单"}'预期返回:JSON{ "code": 200, "msg": "工单创建成功,编号:RP20240501123" }。若返回{"code":500,"msg":"创建失败:..."},检查applicationContext.xml中dataSource的jdbcUrl、用户名密码是否正确,以及 MySQL 是否允许远程连接。
5.3 验证事务回滚是否生效(制造异常场景)
修改RepairOrderService.createRepairOrder()方法,在vehicleMapper.updateById()后手动抛异常:
vehicleMapper.updateById(vehicle); throw new RuntimeException("模拟数据库异常"); // 加这一行再次执行上一条 curl 命令,然后查数据库:
SELECT COUNT(*) FROM repair_order WHERE order_no LIKE 'RP%'; SELECT COUNT(*) FROM vehicle WHERE id = 1;正确结果:两个 COUNT 都为 0 —— 因为事务回滚,工单未插入,车辆时间也未更新。若repair_order表中多了一条记录,说明@Transactional未生效(常见于未用@Autowired注入 service)。
5.4 验证分页功能是否启用 PageHelper
curl "http://localhost:8080/repair/list?pageNum=2&pageSize=5"在RepairOrderMapper.selectListWithVehicle方法中打断点,观察PageHelper.startPage()是否被调用,且 SQL 是否自动追加LIMIT 5,5。若未追加,检查 Controller 方法是否在调用 Mapper 前执行了PageHelper.startPage(pageNum, pageSize),且该方法位于 Service 层(PageHelper 作用域为当前线程)。
这四条命令构成一个最小验证闭环:路由通 → 数据库通 → 事务通 → 分页通。每次代码变更后执行一遍,比等前端联调快 10 倍。
本文还有配套的精品资源,点击获取