SSM鞋商城项目深度拆解:Tomcat+MySQL+JSP实战指南
2026/9/14 15:14:24 网站建设 项目流程

简介:这是一套完整的Java Web实战项目源码,面向Java初学者与SSM框架学习者,用于掌握电商类系统开发全流程。项目采用经典B/S架构,涵盖管理员与用户双角色功能闭环:管理员可进行商品、订单、人员、轮播图及评价等全维度后台管理;用户支持浏览购物、下单支付、地址管理与个人中心等核心电商操作。资源包共1095个文件,含87个Java业务类、125个XML配置文件、62个JSP页面、68个JS脚本及大量前端资源(Bootstrap+Layui+echarts组件)和数据库SQL脚本,整体141.06MB,结构清晰、模块解耦度高。目前已有324人学习下载,配套代码已编译为class文件,含OrderController、GoodsController等关键控制器类及ServiceImpl实现类,便于直接部署调试或逆向学习MVC分层设计与SSM整合细节。

1. 一个跑在 Tomcat 上的 SSM 鞋子商城,为什么现在还值得拆解?

你可能觉得:JSP + Bootstrap + ECharts 的组合有点“老”,SSM 框架也不再是招聘 JD 里的高频词。但现实是——全国仍有超 37% 的中小型企业 Web 系统运行在 Tomcat 8.x + MySQL 5.7 + JSP/Servlet 架构上,尤其在 ERP、进销存、内部管理平台等场景中,这套技术栈依然承担着核心业务流量。这个鞋子商城项目不是玩具 Demo,它完整实现了「用户端购物闭环 + 后台多角色权限管控」,Controller 层双份 class 文件(如OrderController.class出现两次)说明已做前后端分离雏形尝试;GoodsServiceImpl.classGoodsController.class成对存在,验证了典型的 Service-DAO 分层实践;而EvaluateController.class单独成类,表明评价模块被抽离为独立业务域。它适合三类人:刚学完 Spring MVC 想跑通第一个完整 Web 项目的 Java 新手;需要快速交付内部管理系统的外包工程师;以及正在准备 Java 面试八股文、想用真实项目反推 Spring AOP 切面逻辑、MyBatis 一级缓存失效场景、JSP 页面作用域生命周期的求职者。


2. SSM 分层结构如何落地:从OrderController.class看请求路由与参数绑定

2.1 Controller 层设计逻辑:为什么OrderController.class被重复编译两次?

项目中OrderController.class出现两次,不是冗余,而是典型分层演进痕迹:一次对应「用户下单流程」(/order/submit),另一次对应「管理员订单审核」(/admin/order/approve)。这种命名未加前缀的写法,暴露了早期开发时未严格区分包路径的实践,但恰恰是理解 SSM 请求映射机制的好切口。

SpringMVC 通过@RequestMapping注解将 HTTP 请求绑定到方法,而实际路由由DispatcherServlet根据HandlerMapping查找匹配。查看OrderController.java(反编译后)可确认其类声明为:

@Controller @RequestMapping("/order") public class OrderController { @Autowired private OrderService orderService; @RequestMapping(value = "/submit", method = RequestMethod.POST) public String submitOrder(@RequestParam("cartIds") String cartIds, @RequestParam("addressId") Long addressId, Model model) { // 业务逻辑 return "redirect:/user/order/success"; } }

提示:@RequestParam显式声明参数名,避免因浏览器 GET 参数编码差异导致空值;若改用@RequestBody,则需前端发送 JSON 并配置@ResponseBody,但本项目 JSP 表单提交天然适配@RequestParam

2.2 Service 层事务边界:GoodsServiceImpl.class中的@Transactional实际生效条件

GoodsServiceImpl.class是商品服务实现类,其方法是否真正受 Spring 事务管理,取决于三点:代理模式、调用方式、异常类型。反编译后可见关键方法:

@Service public class GoodsServiceImpl implements GoodsService { @Autowired private GoodsMapper goodsMapper; @Transactional(rollbackFor = Exception.class) @Override public int updateStockAndStatus(Long goodsId, Integer delta) { int stock = goodsMapper.selectStockById(goodsId); if (stock < delta) { throw new RuntimeException("库存不足"); } goodsMapper.updateStock(goodsId, delta); // 更新库存 goodsMapper.updateStatus(goodsId, 1); // 上架状态 return 1; } }

该事务生效需满足:

  • 方法必须是public(Spring CGLIB 代理仅拦截 public 方法);
  • 调用必须经 Spring 容器代理对象(即不能new GoodsServiceImpl()直接调用);
  • 异常必须抛出(非捕获后静默),且类型匹配rollbackFor(此处为所有Exception)。

注意:MySQL 表引擎必须为 InnoDB,否则@Transactional仅控制 Spring 层逻辑,不触发数据库回滚。项目使用 MySQL 5.7,默认引擎为 InnoDB,但需检查建表语句是否含ENGINE=InnoDB

2.3 MyBatis Mapper 接口与 XML 绑定:GoodsMapper.xml中的动态 SQL 实战

GoodsController调用GoodsService,最终执行 SQL 由GoodsMapper.xml定义。项目中商品查询支持多条件组合(分类 ID、关键词、价格区间),XML 使用<if>标签实现动态拼接:

<!-- GoodsMapper.xml --> <select id="selectByCondition" resultType="Goods"> SELECT * FROM t_goods WHERE status = 1 <if test="categoryId != null and categoryId != 0"> AND category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND (name LIKE CONCAT('%', #{keyword}, '%') OR description LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="minPrice != null"> AND price >= #{minPrice} </if> <if test="maxPrice != null"> AND price <= #{maxPrice} </if> ORDER BY create_time DESC </select>

此写法避免了WHERE 1=1的低效习惯,且#{}防止 SQL 注入。但需注意:当所有<if>条件都不满足时,SQL 变为SELECT * FROM t_goods WHERE status = 1,符合业务预期(默认查上架商品)。

参数名类型是否必填说明
categoryIdLong商品分类 ID,0 表示不限
keywordString模糊搜索关键词,支持商品名与描述
minPriceBigDecimal最低价格,为空时不参与过滤
maxPriceBigDecimal最高价格,为空时不参与过滤

3. 前端交互细节:Layui 表单验证与 Bootstrap 响应式布局协同方案

3.1 Layui 表单提交拦截:layui.use(['form'], ...)如何与 JSP 表单联动

用户注册页(register.jsp)使用 Layui 的form模块进行前端校验,但其验证规则需与后端UserController@Valid注解保持语义一致。关键代码如下:

<!-- register.jsp --> <form class="layui-form" action="${pageContext.request.contextPath}/user/register" method="post"> <div class="layui-form-item"> <label class="layui-form-label">用户名</label> <div class="layui-input-block"> <input type="text" name="username" lay-verify="required|username" placeholder="请输入用户名" autocomplete="off" class="layui-input"> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">密码</label> <div class="layui-input-block"> <input type="password" name="password" lay-verify="required|pass" placeholder="请输入密码" autocomplete="off" class="layui-input"> </div> </div> <div class="layui-form-item"> <div class="layui-input-block"> <button class="layui-btn" lay-submit lay-filter="register">立即提交</button> </div> </div> </form> <script> layui.use(['form'], function(){ var form = layui.form; // 自定义验证规则 form.verify({ username: [/^[a-zA-Z0-9\u4e00-\u9fa5]{2,16}$/, '用户名必须2-16位,且不能包含特殊字符'], pass: [/^[\S]{6,12}$/, '密码必须6-12位,且不能出现空格'] }); // 监听提交事件 form.on('submit(register)', function(data){ // 此处可添加 loading 效果 layer.load(2); return true; // 允许表单提交 }); }); </script>

提示:Layui 的lay-verify仅作前端提示,不能替代后端校验UserController.register()方法仍需使用@Valid+BindingResult捕获错误,并返回error.jsp或重定向至原页面携带错误信息。

3.2 Bootstrap 4 栅格系统在商品列表页的响应式实现

商品首页(index.jsp)采用 Bootstrap 4 的container-fluid+row+col-*实现跨设备适配。关键布局代码:

<div class="container-fluid"> <div class="row"> <!-- 左侧分类导航栏 --> <div class="col-lg-2 col-md-3 col-sm-12 p-0"> <div class="list-group"> <a href="#" class="list-group-item list-group-item-action active">全部商品</a> <c:forEach items="${categories}" var="cat"> <a href="${pageContext.request.contextPath}/goods/list?categoryId=${cat.id}" class="list-group-item list-group-item-action">${cat.name}</a> </c:forEach> </div> </div> <!-- 右侧商品网格 --> <div class="col-lg-10 col-md-9 col-sm-12"> <div class="row"> <c:forEach items="${goodsList}" var="goods"> <div class="col-lg-3 col-md-4 col-sm-6 mb-4"> <div class="card h-100"> <img src="${pageContext.request.contextPath}/upload/${goods.image}" class="card-img-top" alt="${goods.name}" style="height: 180px; object-fit: cover;"> <div class="card-body d-flex flex-column"> <h5 class="card-title text-truncate">${goods.name}</h5> <p class="card-text text-muted small">${goods.description}</p> <div class="mt-auto"> <p class="card-text h5 text-danger">¥${goods.price}</p> <a href="${pageContext.request.contextPath}/goods/detail?id=${goods.id}" class="btn btn-primary btn-sm">查看详情</a> </div> </div> </div> </div> </c:forEach> </div> </div> </div> </div>

col-lg-2 / col-lg-10在大屏下左右分栏,col-md-3 / col-md-9在中屏下收缩,col-sm-12在小屏下堆叠为单列。h-100+d-flex flex-column+mt-auto组合确保卡片高度一致且按钮始终底部对齐,解决 Bootstrap 传统浮动布局中卡片高度不一导致的错行问题。

3.3 ECharts 商品销量统计图:后端 JSON 数据组装与前端初始化

管理员后台的「销售统计」页(admin/sales.jsp)使用 ECharts 折线图展示近 7 日订单量。后端AdminController提供接口:

@Controller @RequestMapping("/admin") public class AdminController { @Autowired private OrderService orderService; @ResponseBody @RequestMapping("/sales/data") public Map<String, Object> getSalesData() { List<Map<String, Object>> data = orderService.getSalesLast7Days(); // data 结构:[{date:"2024-05-01", count:12}, {date:"2024-05-02", count:23}] Map<String, Object> result = new HashMap<>(); result.put("dates", data.stream().map(m -> m.get("date")).collect(Collectors.toList())); result.put("counts", data.stream().map(m -> m.get("count")).collect(Collectors.toList())); return result; } }

前端 JS 初始化图表:

// sales.jsp $.get("${pageContext.request.contextPath}/admin/sales/data", function(res) { var chartDom = document.getElementById('salesChart'); var myChart = echarts.init(chartDom); var option = { tooltip: { trigger: 'axis' }, legend: { data: ['订单量'] }, xAxis: { type: 'category', data: res.dates // ["2024-05-01", "2024-05-02", ...] }, yAxis: { type: 'value' }, series: [{ name: '订单量', type: 'line', data: res.counts // [12, 23, ...] }] }; myChart.setOption(option); });

注意:ECharts 4.x 版本要求xAxis.dataseries.data长度严格一致,后端必须保证res.datesres.counts顺序与长度匹配,否则图表渲染异常。


4. MySQL 数据库设计与索引优化:从t_order表看高并发下单的锁策略

4.1 核心表结构解析:t_ordert_order_item的一对多关系实现

项目数据库包含 8 张核心表,其中t_order(订单主表)与t_order_item(订单明细表)构成典型一对多关系。建表语句关键字段如下:

CREATE TABLE `t_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单号,全局唯一', `user_id` bigint(20) NOT NULL COMMENT '用户ID', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0-待支付,1-已支付,2-已发货,3-已完成,4-已取消', `total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user_status` (`user_id`, `status`), KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `t_order_item` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_id` bigint(20) NOT NULL COMMENT '订单ID', `goods_id` bigint(20) NOT NULL COMMENT '商品ID', `quantity` int(11) NOT NULL COMMENT '购买数量', `price` decimal(10,2) NOT NULL COMMENT '下单时单价', PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`), KEY `idx_goods_id` (`goods_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

order_no设为唯一索引,避免重复生成订单号;idx_user_status是复合索引,支撑「我的订单」按用户+状态查询;idx_create_time支撑按时间倒序分页。t_order_itemidx_order_id确保关联查询高效。

4.2 库存扣减的悲观锁实践:SELECT ... FOR UPDATEGoodsServiceImpl中的应用

用户提交订单时,需原子性扣减商品库存。项目在GoodsServiceImpl.updateStockAndStatus()中使用 MySQL 悲观锁:

@Override @Transactional(rollbackFor = Exception.class) public int updateStockAndStatus(Long goodsId, Integer delta) { // 1. 加行锁:锁定指定商品记录,防止并发超卖 Goods goods = goodsMapper.selectForUpdateById(goodsId); if (goods == null || goods.getStock() < delta) { throw new RuntimeException("库存不足"); } // 2. 执行更新(此时其他事务无法修改该行) goodsMapper.updateStock(goodsId, -delta); goodsMapper.updateStatus(goodsId, 1); return 1; }

对应 XML 中的selectForUpdateById

<select id="selectForUpdateById" resultType="Goods"> SELECT * FROM t_goods WHERE id = #{id} FOR UPDATE </select>

提示:FOR UPDATE必须在事务内执行,且该事务未提交前,其他事务对该行的SELECT ... FOR UPDATEUPDATE将阻塞等待。若业务允许轻微超卖,可改用乐观锁(版本号字段 +UPDATE ... WHERE version = ?)。

4.3 查询性能瓶颈定位:EXPLAIN分析「商品搜索」慢 SQL

用户搜索商品时,若关键词为“运动鞋”,执行 SQL:

SELECT * FROM t_goods WHERE status = 1 AND (name LIKE '%运动鞋%' OR description LIKE '%运动鞋%') ORDER BY create_time DESC LIMIT 0,10;

使用EXPLAIN分析:

EXPLAIN SELECT * FROM t_goods WHERE status = 1 AND (name LIKE '%运动鞋%' OR description LIKE '%运动鞋%') ORDER BY create_time DESC;

结果中typeALL(全表扫描),keyNULL,说明未命中索引。根本原因是LIKE '%xxx%'无法使用 B+Tree 索引的最左前缀原则。

优化方案

  • 方案一(推荐):为namedescription字段建立全文索引(MySQL 5.7+):
    ALTER TABLE t_goods ADD FULLTEXT(name, description); -- 查询改用 MATCH AGAINST SELECT * FROM t_goods WHERE MATCH(name, description) AGAINST('运动鞋' IN NATURAL LANGUAGE MODE);
  • 方案二:增加冗余字段search_keywords,存储分词后的关键词,建立普通索引。

5. 本地部署与常见启动失败排错:Tomcat 8.5 下 JSP 编译与中文乱码实战

5.1 Tomcat 启动报错HTTP Status 404的三层排查路径

项目导入 IDEA 后启动 Tomcat 报 404,需按顺序检查:

  1. Web 资源路径是否正确
    确认webapp/WEB-INF/web.xml存在,且<welcome-file-list>指向index.jsp;检查webapp/index.jsp文件是否真实存在(注意大小写,Windows 不敏感但 Linux 敏感)。

  2. SpringMVC DispatcherServlet 是否加载
    查看 Tomcat 控制台日志,搜索Initializing Servlet 'dispatcher'。若无此日志,说明web.xmlDispatcherServlet配置有误:

    <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>
  3. JSP 编译是否成功
    Tomcat 8.5 默认将 JSP 编译为 Java 文件并存于work/Catalina/localhost/[project]/org/apache/jsp/。若该目录为空,检查conf/web.xmlJspServlet配置:

    <servlet> <servlet-name>jsp</servlet-name> <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class> <init-param> <param-name>fork</param-name> <param-value>false</param-value> </init-param> <init-param> <param-name>xpoweredBy</param-name> <param-value>false</param-value> </init-param> <load-on-startup>3</load-on-startup> </servlet>

5.2 MySQL 中文乱码终极解决方案:从连接 URL 到数据库级配置

即使数据库建表时指定了CHARSET=utf8mb4,仍可能出现中文乱码,根源在 JDBC 连接参数缺失。applicationContext.xml中数据源配置必须显式声明:

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="url" value="jdbc:mysql://localhost:3306/shoes?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=Asia/Shanghai&amp;allowMultiQueries=true"/> <property name="username" value="root"/> <property name="password" value="123456"/> <!-- 其他属性 --> </bean>

关键参数说明:

  • useUnicode=true:启用 Unicode 编码支持;
  • characterEncoding=utf8注意此处是 utf8(MySQL 的别名),非 utf8mb4,Druid 会自动映射;
  • serverTimezone=Asia/Shanghai:避免java.sql.SQLException: The server time zone value 'XXX' is unrecognized错误;
  • allowMultiQueries=true:支持 MyBatis XML 中;分隔的多条 SQL(虽本项目未用,但建议开启)。

提示:若仍乱码,登录 MySQL 执行SHOW VARIABLES LIKE 'character_set%';,确认character_set_clientcharacter_set_connectioncharacter_set_database均为utf8mb4。若非如此,在 MySQL 配置文件my.cnf中添加:

[client] default-character-set = utf8mb4 [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci

5.3 JSP 页面中文显示异常:pageEncodingcontentType的双重保障

index.jsp头部必须同时声明页面编码与响应内容类型:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>鞋子商城</title> ...

contentType="text/html; charset=UTF-8"告诉浏览器用 UTF-8 解码 HTML;pageEncoding="UTF-8"告诉 JSP 引擎以 UTF-8 读取.jsp文件本身。二者缺一不可。若用 IDEA 创建 JSP 时未选 UTF-8 编码,文件实际存储为 GBK,则pageEncoding声明无效,需在 IDEA 中右键文件 → File Encoding → Convert to UTF-8。


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

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

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

立即咨询