简介:这是一套完整的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.class与GoodsController.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,符合业务预期(默认查上架商品)。
| 参数名 | 类型 | 是否必填 | 说明 |
|---|---|---|---|
categoryId | Long | 否 | 商品分类 ID,0 表示不限 |
keyword | String | 否 | 模糊搜索关键词,支持商品名与描述 |
minPrice | BigDecimal | 否 | 最低价格,为空时不参与过滤 |
maxPrice | BigDecimal | 否 | 最高价格,为空时不参与过滤 |
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.data与series.data长度严格一致,后端必须保证res.dates和res.counts顺序与长度匹配,否则图表渲染异常。
4. MySQL 数据库设计与索引优化:从t_order表看高并发下单的锁策略
4.1 核心表结构解析:t_order与t_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_item的idx_order_id确保关联查询高效。
4.2 库存扣减的悲观锁实践:SELECT ... FOR UPDATE在GoodsServiceImpl中的应用
用户提交订单时,需原子性扣减商品库存。项目在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 UPDATE或UPDATE将阻塞等待。若业务允许轻微超卖,可改用乐观锁(版本号字段 +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;结果中type为ALL(全表扫描),key为NULL,说明未命中索引。根本原因是LIKE '%xxx%'无法使用 B+Tree 索引的最左前缀原则。
优化方案:
- 方案一(推荐):为
name和description字段建立全文索引(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,需按顺序检查:
Web 资源路径是否正确
确认webapp/WEB-INF/web.xml存在,且<welcome-file-list>指向index.jsp;检查webapp/index.jsp文件是否真实存在(注意大小写,Windows 不敏感但 Linux 敏感)。SpringMVC DispatcherServlet 是否加载
查看 Tomcat 控制台日志,搜索Initializing Servlet 'dispatcher'。若无此日志,说明web.xml中DispatcherServlet配置有误:<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>JSP 编译是否成功
Tomcat 8.5 默认将 JSP 编译为 Java 文件并存于work/Catalina/localhost/[project]/org/apache/jsp/。若该目录为空,检查conf/web.xml中JspServlet配置:<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&characterEncoding=utf8&serverTimezone=Asia/Shanghai&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_client、character_set_connection、character_set_database均为utf8mb4。若非如此,在 MySQL 配置文件my.cnf中添加:[client] default-character-set = utf8mb4 [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci
5.3 JSP 页面中文显示异常:pageEncoding与contentType的双重保障
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。
本文还有配套的精品资源,点击获取