
简介这是一套基于SSM框架的汽车配件商城系统完整源码面向Java Web初学者与课程设计、毕业设计开发者帮助快速理解前后台分离的电商业务实现。项目分为普通用户前台与管理员后台用户可完成注册登录、浏览首页、查看商品详情、加入购物车、确认订单、提交付款及查看个人订单管理员则负责人员管理、配件管理、订单管理、图片与图表管理及个人信息维护覆盖典型商城核心流程。压缩包共626个文件约8.99MB包含91个java源文件、22个jsp页面、32个js脚本、26个css样式及80张jpg图片另有xml配置、sql建库脚本与properties资源文件结构完整便于二次开发。技术栈为SpringSpringMVCMyBatis后端与JSPjQuery前端运行需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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 配件表基础信息 库存冗余字段 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 适配关系表车型与配件的多对多桥梁 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单主表 订单明细 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明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 PageInfoPartVO listByCarModel(Integer carModelId, int page, int size) { PageHelper.startPage(page, size); ListPartVO list partMapper.selectByCarModelId(carModelId); return new PageInfo(list); } Transactional(rollbackFor Exception.class) Override public void savePartWithFit(Part part, ListInteger 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?carModelIdxx返回配件分页。前端三个 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 tdbutton classbtn-add>// 加入购物车Session 中维护 MappartId, CartItem RequestMapping(/cart/add) ResponseBody public Result addToCart(RequestParam Integer partId, RequestParam Integer carModelId, RequestParam(defaultValue 1) Integer qty, HttpSession session) { MapInteger, CartItem cart (MapInteger, 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, MapInteger, CartItem cart) { if (cart null || cart.isEmpty()) { throw new BizException(购物车为空); } String orderNo OrderNoGenerator.next(); BigDecimal total BigDecimal.ZERO; ListOrderItem 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 stockstock-#{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() { ListOrders list orderMapper.selectTimeout(30); for (Orders o : list) { orderMapper.updateStatus(o.getId(), 4); // 4已取消 // 回滚库存 ListOrderItem 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 contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %Tomcatserver.xml的 Connector 加URIEncodingUTF-8JDBC 连接串加?useUnicodetruecharacterEncodingutf8mb4建库建表统一utf8mb4。四处对齐乱码必消。5.2 车型联动查不出数据AJAX 返回 200 但回调不执行现象浏览器 Network 里接口返回 200响应体也有 JSON但$.get的回调死活不进。原因后端返回的Content-Type是text/html而不是application/jsonjQuery 按声明类型解析失败静默丢弃。解决Controller 方法上加ResponseBodySpring 会自动设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 页面报 404web.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-fileindex.jsp/welcome-file确认文件真实存在检查WEB-INF下web.xml有没有被 IDE 生成到错误目录。6. 从课程设计到能上线的距离三个我踩过的进阶技巧这套系统跑通之后很多人会问「能不能真上线」。我的答案是能但得补三块。第一块是连接池别用 DriverManager 裸连上 Druid 或 HikariCPinitialSize设 5、maxActive设 20MySQL 的max_connections默认 151够用。第二块是 SQL 日志MyBatis 配setting namelogImpl valueSTDOUT_LOGGING/开发期把执行的 SQL 和参数打出来比猜快十倍。第三块是打包传统 JSP 项目打 war 包丢 Tomcatwebapps下别打 jarJSP 在 jar 里跑不起来。!-- MyBatis 开发期日志配置 -- configuration settings setting namelogImpl valueSTDOUT_LOGGING/ setting namemapUnderscoreToCamelCase valuetrue/ /settings /configurationmapUnderscoreToCamelCase这个配置特别值数据库part_no自动映射到 Java 的partNo省掉一堆resultMap手写映射少写几百行样板代码。验证方法上我习惯用 Postman 先把接口全跑一遍再开页面联调。接口层没问题页面出问题就一定是 JSP 或 jQuery 的锅排查范围直接砍一半。另外配件适配关系这种核心数据上线前一定要写个脚本校验有没有配件没有任何车型适配、有没有车型下没有任何配件这两种脏数据在演示时最容易被问到。最后说个习惯。我做这类项目永远先在纸上把「车型—配件—订单」的 ER 图和数据流画一遍确认字段和关系再动手建表。表结构改一次Mapper、Service、JSP 全得跟着动返工成本极高。这套 SSM 架构不新但把数据模型和事务边界这两件事做扎实它照样能撑起一个正经的配件商城。希望帮到你。本文还有配套的精品资源点击获取