☰
SSM+JSP汽车配件商城实战:车型适配与订单事务
2026/9/28 16:42:13 网站建设 项目流程

简介:这是一套基于SSM框架的汽车配件商城系统完整源码,面向Java Web初学者与课程设计、毕业设计开发者,帮助快速理解前后台分离的电商业务实现。项目分为普通用户前台与管理员后台:用户可完成注册登录、浏览首页、查看商品详情、加入购物车、确认订单、提交付款及查看个人订单;管理员则负责人员管理、配件管理、订单管理、图片与图表管理及个人信息维护,覆盖典型商城核心流程。压缩包共626个文件,约8.99MB,包含91个java源文件、22个jsp页面、32个js脚本、26个css样式及80张jpg图片,另有xml配置、sql建库脚本与properties资源文件,结构完整便于二次开发。技术栈为Spring+SpringMVC+MyBatis后端与JSP+jQuery前端,运行需JDK1.8、Tomcat及MySQL,支持IDEA、Eclipse等IDE导入。目前已有212人学习,适合作为SSM实战练手与功能扩展的参考模板。

1. 汽车配件商城系统:从车型匹配到订单落库,一套 SSM 老架构还能怎么打

做汽车配件这行有个反直觉的地方:SKU 数量比普通电商多一个维度。普通商城一件商品就是一个 SKU,配件商城得先锁定「车型年款」,再谈「配件适配」,最后才是价格和库存。我见过太多团队用通用商城模板硬套配件业务,结果车型筛选做成三级联动就卡死,订单里存的是配件名而不是适配关系,售后一查一个不吱声。

这套基于 Java + SSM + JSP + jQuery + MySQL 的汽车配件商城系统,解决的正是「车型—配件—订单」这条链路的落地问题。SSM 负责业务分层和事务,JSP 承担后台管理与前台展示,jQuery 处理车型联动和表格交互,MySQL 存适配关系和库存流水。它适合两类人:一是做 JavaWeb 课程设计或毕设、需要一套完整可跑案例的在校开发者;二是接手传统 JSP 项目、要在老架构上继续迭代的一线工程师。下面按「先立住选型、再跑通链路、最后避坑」的顺序拆开讲。

2. 车型适配模型怎么建:SSM 分层与 MySQL 表结构设计

配件商城和普通商城最大的区别在数据模型。普通商城是「商品—分类—订单」三层,配件商城得插进「车型」这一维,而且车型和配件是多对多:一个刹车片适配多个年款,一个年款又有多个配件。这一章先把模型立住,后面所有业务都从这里长出来。

2.1 为什么用 SSM 而不是 SpringBoot 重写

热词里 springboot2 集成 jsp 的搜索量一直不低,很多人第一反应是「老项目直接上 SpringBoot 算了」。我的建议是:如果目标是课程设计、毕设或者维护存量系统,别动。原因有三。

第一,SSM 的分层边界比 SpringBoot 更「显式」。SpringBoot 靠自动配置把大量 Bean 藏起来,新手调事务传播行为时经常找不到北;SSM 的 applicationContext.xml 里每个 Bean、每个 AOP 切面都摆在明面上,出问题能顺着配置一路查。第二,JSP 在 SpringBoot 里属于「二等公民」,官方推荐 Thymeleaf,集成 JSP 要额外配 InternalResourceViewResolver,打 war 包还得改打包方式,纯属给自己找事。第三,课程设计和毕设的评分点往往就在「SSM 整合是否规范」,用 SpringBoot 反而丢了展示分。

所以选型结论很直接:业务复杂度不高、要展示分层能力、要跑在 Tomcat 上,SSM + JSP 是性价比最高的组合。jQuery 同理,车型三级联动、DataTable 渲染订单列表这些场景,jQuery 写起来比引入 Vue 全家桶快得多,老项目里也没有构建工具链的负担。

2.2 车型—配件—订单的核心表结构

先把表建对,后面代码才不拧巴。核心是四张主表加两张关联表,字段和索引我按实际踩过的坑给出来。

-- 车型表:按品牌、车系、年款三级组织 CREATE TABLE `car_model` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `brand` VARCHAR(50) NOT NULL COMMENT '品牌,如大众', `series` VARCHAR(50) NOT NULL COMMENT '车系,如朗逸', `year` VARCHAR(20) NOT NULL COMMENT '年款,如2018-2020', `engine` VARCHAR(50) DEFAULT NULL COMMENT '排量,如1.4T', KEY `idx_brand_series` (`brand`, `series`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 配件表:基础信息 + 库存冗余字段 CREATE TABLE `part` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `part_no` VARCHAR(50) NOT NULL COMMENT '配件编号,唯一', `name` VARCHAR(100) NOT NULL, `category_id` INT NOT NULL COMMENT '配件分类', `price` DECIMAL(10,2) NOT NULL DEFAULT 0.00, `stock` INT NOT NULL DEFAULT 0, `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架', UNIQUE KEY `uk_part_no` (`part_no`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 适配关系表:车型与配件的多对多桥梁 CREATE TABLE `part_car_fit` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `part_id` INT NOT NULL, `car_model_id` INT NOT NULL, UNIQUE KEY `uk_part_car` (`part_id`, `car_model_id`), KEY `idx_car_model` (`car_model_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单主表 + 订单明细 CREATE TABLE `orders` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL, `user_id` INT NOT NULL, `total_amount` DECIMAL(10,2) NOT NULL, `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待付 1已付 2已发 3完成', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `order_item` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `order_id` INT NOT NULL, `part_id` INT NOT NULL, `car_model_id` INT DEFAULT NULL COMMENT '下单时锁定的适配车型', `price` DECIMAL(10,2) NOT NULL, `quantity` INT NOT NULL, KEY `idx_order` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

逻辑说明:part_car_fit是整套系统的命门,前台车型筛选、后台配件录入、订单适配校验全走它。order_item里冗余一个car_model_id,是为了售后时能还原「当时买的是适配哪个年款的」,别小看这个字段,退货纠纷里它就是后悔药。

参数说明:字符集统一utf8mb4,配件名里常有特殊符号;price用DECIMAL不用FLOAT,金额计算不能有精度漂移;part_no加唯一索引,防止同一配件重复录入;idx_brand_series复合索引支撑前台三级联动查询。

2.3 SSM 三层怎么切:Controller 只做参数校验

分层切不好,后期改一个需求要动五个文件。我的切法是:Controller 只做参数接收和校验,Service 承担业务编排和事务,DAO 只做单表 CRUD,跨表查询写在 Service 里拼装。

// Controller:只校验参数,不写业务 @Controller @RequestMapping("/part") public class PartController { @Autowired private PartService partService; @RequestMapping("/listByCar") @ResponseBody public Result listByCar(@RequestParam Integer carModelId, @RequestParam(defaultValue = "1") Integer page) { if (carModelId == null || carModelId <= 0) { return Result.fail("车型参数非法"); } return Result.ok(partService.listByCarModel(carModelId, page, 10)); } } // Service:业务编排 + 事务边界 @Service public class PartServiceImpl implements PartService { @Autowired private PartMapper partMapper; @Autowired private PartCarFitMapper fitMapper; @Override public PageInfo<PartVO> listByCarModel(Integer carModelId, int page, int size) { PageHelper.startPage(page, size); List<PartVO> list = partMapper.selectByCarModelId(carModelId); return new PageInfo<>(list); } @Transactional(rollbackFor = Exception.class) @Override public void savePartWithFit(Part part, List<Integer> carModelIds) { partMapper.insert(part); for (Integer carId : carModelIds) { fitMapper.insert(new PartCarFit(part.getId(), carId)); } } }

逻辑说明:listByCarModel用 PageHelper 做物理分页,避免全表查出来再内存分页;savePartWithFit把「插配件 + 插适配关系」放进同一个事务,任何一步失败整体回滚,否则会出现配件存在但没有任何车型适配的脏数据。

参数说明:@Transactional的rollbackFor = Exception.class必须显式写,默认只回滚RuntimeException,检查型异常不回滚是经典翻车点;分页 size 建议固定 10 到 20,配件列表字段多,一次渲染太多行 jQuery DataTable 会卡。

3. 前台车型联动与配件检索:JSP + jQuery 怎么把三级联动做顺

模型建好之后,前台第一道坎就是车型三级联动。用户选品牌、车系、年款,页面要实时把配件列表刷出来。这块用 JSP 渲染骨架、jQuery 发异步请求,是最省事的组合。

3.1 三级联动的 jQuery 实现与接口约定

先约定接口:/car/series?brand=大众返回车系列表,/car/year?brand=大众&series=朗逸返回年款列表,/part/listByCar?carModelId=xx返回配件分页。前端三个 select 依次联动。

// 品牌变化 -> 加载车系 $('#brand').change(function () { var brand = $(this).val(); if (!brand) { $('#series').empty(); return; } $.get('/car/series', { brand: brand }, function (res) { var $series = $('#series').empty().append('<option value="">请选择车系</option>'); $.each(res.data, function (i, s) { $series.append('<option value="' + s + '">' + s + '</option>'); }); $('#year').empty(); // 车系变了,年款必须清空 }, 'json'); }); // 车系变化 -> 加载年款 $('#series').change(function () { var brand = $('#brand').val(), series = $(this).val(); if (!series) { $('#year').empty(); return; } $.get('/car/year', { brand: brand, series: series }, function (res) { var $year = $('#year').empty().append('<option value="">请选择年款</option>'); $.each(res.data, function (i, y) { $year.append('<option value="' + y.id + '">function renderPartTable(list) { var $tbody = $('#partTable tbody').empty(); $.each(list, function (i, p) { var name = p.name.length > 20 ? p.name.substring(0, 20) + '...' : p.name; var fitDesc = p.fitDesc || ''; var fitShow = fitDesc.length > 15 ? fitDesc.substring(0, 15) + '...' : fitDesc; $tbody.append( '<tr>' + '<td title="' + p.name + '">' + name + '</td>' + '<td>' + p.partNo + '</td>' + '<td>¥' + p.price + '</td>' + '<td>' + p.stock + '</td>' + '<td title="' + fitDesc + '">' + fitShow + '</td>' + '<td><button class="btn-add">// 加入购物车:Session 中维护 Map<partId, CartItem> @RequestMapping("/cart/add") @ResponseBody public Result addToCart(@RequestParam Integer partId, @RequestParam Integer carModelId, @RequestParam(defaultValue = "1") Integer qty, HttpSession session) { Map<Integer, CartItem> cart = (Map<Integer, CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new HashMap<>(); session.setAttribute("cart", cart); } CartItem item = cart.get(partId); if (item == null) { item = new CartItem(partId, carModelId, qty); cart.put(partId, item); } else { item.setQuantity(item.getQuantity() + qty); } return Result.ok(cart.size()); }

逻辑说明:购物车 key 用partId,同一配件重复加购只累加数量。carModelId存在 CartItem 里,下单时写入order_item,保证适配关系不丢。

参数说明:Session 超时时间在web.xml里配,默认 30 分钟,配件选购耗时长可以调到 60 分钟;集群部署时 Session 要共享,单机课程设计不用管。

4. 订单落库与库存扣减:Service 事务边界怎么划才不出脏数据

订单是整套系统最容易出问题的地方。库存扣减、订单写入、购物车清空,这三步必须原子化。这一章讲事务边界怎么划,以及并发下怎么防超卖。

4.1 下单主流程的事务编排

下单逻辑我一般写成「校验库存 → 扣库存 → 写订单 → 写明细 → 清购物车」,全部包在一个事务里。

@Transactional(rollbackFor = Exception.class) @Override public String createOrder(Integer userId, Map<Integer, CartItem> cart) { if (cart == null || cart.isEmpty()) { throw new BizException("购物车为空"); } String orderNo = OrderNoGenerator.next(); BigDecimal total = BigDecimal.ZERO; List<OrderItem> items = new ArrayList<>(); for (CartItem ci : cart.values()) { // 悲观锁查库存,防止并发超卖 Part part = partMapper.selectForUpdate(ci.getPartId()); if (part == null || part.getStatus() != 1) { throw new BizException("配件已下架:" + ci.getPartId()); } if (part.getStock() < ci.getQuantity()) { throw new BizException("库存不足:" + part.getName()); } // 扣库存 int affected = partMapper.reduceStock(ci.getPartId(), ci.getQuantity()); if (affected == 0) { throw new BizException("库存扣减失败,请重试"); } total = total.add(part.getPrice().multiply(new BigDecimal(ci.getQuantity()))); items.add(new OrderItem(ci.getPartId(), ci.getCarModelId(), part.getPrice(), ci.getQuantity())); } Orders order = new Orders(orderNo, userId, total, 0); orderMapper.insert(order); for (OrderItem item : items) { item.setOrderId(order.getId()); orderItemMapper.insert(item); } return orderNo; }

逻辑说明:selectForUpdate走SELECT ... FOR UPDATE,在事务内锁住配件行,防止两个请求同时读到相同库存然后都扣成功。reduceStock的 SQL 带stock >= #{qty}条件,返回影响行数为 0 说明库存不够,直接抛异常回滚。

参数说明:OrderNoGenerator用「时间戳 + 用户 ID 后四位 + 随机数」生成,长度控制在 32 以内;BizException继承RuntimeException,保证事务能回滚。

4.2 库存扣减的两种写法对比

方案SQL 写法优点缺点适用场景
悲观锁SELECT ... FOR UPDATE再 UPDATE逻辑直观,强一致锁行,并发低课程设计、低并发
乐观锁UPDATE ... SET stock=stock-#{qty} WHERE id=#{id} AND stock>=#{qty}无锁,并发高需判断影响行数中小型生产
Redis 预扣先扣 Redis 再异步落库性能最高一致性复杂高并发大促

课程设计和毕设用悲观锁足够,代码好懂、评分点清晰。生产环境如果 QPS 上来了,换成乐观锁那条 UPDATE 语句,把selectForUpdate去掉,靠影响行数判断即可。

4.3 订单状态流转与超时取消

订单状态从 0 待付到 3 完成,中间要有超时取消。JSP 项目里没有现成的定时任务框架,用 Spring 的@Scheduled最省事。

@Component public class OrderTimeoutTask { @Autowired private OrderMapper orderMapper; @Autowired private PartMapper partMapper; // 每 5 分钟扫一次,取消 30 分钟未支付订单 @Scheduled(cron = "0 0/5 * * * ?") @Transactional(rollbackFor = Exception.class) public void cancelTimeoutOrders() { List<Orders> list = orderMapper.selectTimeout(30); for (Orders o : list) { orderMapper.updateStatus(o.getId(), 4); // 4=已取消 // 回滚库存 List<OrderItem> items = orderItemMapper.selectByOrderId(o.getId()); for (OrderItem item : items) { partMapper.addStock(item.getPartId(), item.getQuantity()); } } } }

逻辑说明:定时任务扫出超时订单,改状态为已取消,同时把库存加回去。回滚库存必须和改状态在同一事务,否则会出现订单取消了但库存没还的脏数据。

参数说明:cron表达式0 0/5 * * * ?表示每 5 分钟执行一次;超时阈值 30 分钟按业务调,配件商城用户比价时间长,可以放宽到 60 分钟。@Scheduled需要在 Spring 配置里加<task:annotation-driven/>。

5. 避坑与排查:JSP + SSM 项目里最容易翻车的 5 个点

这套架构跑通不难,难的是细节。下面 5 个坑我都实际踩过,按「现象 → 原因 → 解决」写清楚。

5.1 中文乱码:配件名存进库变成问号

现象:前台录入「刹车片」,数据库里显示???,页面回显也是乱码。

原因:三层编码没统一。JSP 页面pageEncoding、Tomcat 的URIEncoding、MySQL 连接串的characterEncoding、数据库和表的字符集,任何一层是ISO-8859-1或gbk都会乱。

解决:JSP 头部写<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>;Tomcatserver.xml的 Connector 加URIEncoding="UTF-8";JDBC 连接串加?useUnicode=true&characterEncoding=utf8mb4;建库建表统一utf8mb4。四处对齐,乱码必消。

5.2 车型联动查不出数据:AJAX 返回 200 但回调不执行

现象:浏览器 Network 里接口返回 200,响应体也有 JSON,但$.get的回调死活不进。

原因:后端返回的Content-Type是text/html而不是application/json,jQuery 按声明类型解析失败,静默丢弃。

解决:Controller 方法上加@ResponseBody,Spring 会自动设application/json;如果用了@RestController就不用加。另外检查jackson-databind依赖是否引入,缺了它@ResponseBody返回对象会报 406。

5.3 订单重复提交:用户狂点下单按钮

现象:一次下单生成两条订单,库存扣了两次。

原因:前端按钮没禁用,后端没做幂等。用户网络慢时连点,两个请求都进了 Service。

解决:前端点击后立即$(this).prop('disabled', true);后端在 Session 里放一个 token,下单时校验并删除,同一 token 只能用一次。更彻底的做法是订单号加唯一索引,重复插入直接报错回滚。

5.4 库存扣成负数:并发下的超卖

现象:库存 1 件,两个用户同时下单,都成功了,库存变成 -1。

原因:先SELECT查库存再UPDATE扣减,两步之间没有锁,两个事务读到相同库存。

解决:用UPDATE part SET stock = stock - #{qty} WHERE id = #{id} AND stock >= #{qty},靠影响行数判断;或者SELECT ... FOR UPDATE锁行。前者性能好,后者逻辑直观,按并发量选。

5.5 JSP 页面报 404:web.xml 版本与 Servlet 版本不匹配

现象:Tomcat 启动正常,访问 JSP 报 404,控制台无异常。

原因:web.xml声明的 Servlet 版本和 Tomcat 实际支持的版本对不上,或者web.xml里<welcome-file>配错。

解决:Tomcat 8/9 用web-app 3.1或4.0的 schema,别抄网上的 2.5 老模板;<welcome-file>index.jsp</welcome-file>确认文件真实存在;检查WEB-INF下web.xml有没有被 IDE 生成到错误目录。

6. 从课程设计到能上线的距离:三个我踩过的进阶技巧

这套系统跑通之后,很多人会问「能不能真上线」。我的答案是能,但得补三块。第一块是连接池,别用 DriverManager 裸连,上 Druid 或 HikariCP,initialSize设 5、maxActive设 20,MySQL 的max_connections默认 151,够用。第二块是 SQL 日志,MyBatis 配<setting name="logImpl" value="STDOUT_LOGGING"/>,开发期把执行的 SQL 和参数打出来,比猜快十倍。第三块是打包,传统 JSP 项目打 war 包丢 Tomcatwebapps下,别打 jar,JSP 在 jar 里跑不起来。

<!-- MyBatis 开发期日志配置 --> <configuration> <settings> <setting name="logImpl" value="STDOUT_LOGGING"/> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings> </configuration>

mapUnderscoreToCamelCase这个配置特别值,数据库part_no自动映射到 Java 的partNo,省掉一堆resultMap手写映射,少写几百行样板代码。

验证方法上,我习惯用 Postman 先把接口全跑一遍,再开页面联调。接口层没问题,页面出问题就一定是 JSP 或 jQuery 的锅,排查范围直接砍一半。另外,配件适配关系这种核心数据,上线前一定要写个脚本校验:有没有配件没有任何车型适配、有没有车型下没有任何配件,这两种脏数据在演示时最容易被问到。

最后说个习惯。我做这类项目,永远先在纸上把「车型—配件—订单」的 ER 图和数据流画一遍,确认字段和关系再动手建表。表结构改一次,Mapper、Service、JSP 全得跟着动,返工成本极高。这套 SSM 架构不新,但把数据模型和事务边界这两件事做扎实,它照样能撑起一个正经的配件商城。希望帮到你。

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

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

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

立即咨询