简介:这是一套基于SSM(Spring+SpringMVC+MyBatis)框架与MySQL数据库开发的完整线上商城系统,含前台用户端与后台管理平台,面向Java Web初学者及中级开发者,适用于课程设计、毕业项目或SSM技术栈实战训练。资源包共341个文件,涵盖49个核心Java业务与控制层代码、35个HTML页面、51个JS交互脚本、129个GIF动效资源(多用于后台管理界面提示与加载)、21个JPG与17个PNG图片素材,以及15个XML配置文件(含Spring与MyBatis映射)、1个SQL建库建表脚本和1个start.bat启动批处理,整体压缩后仅1.71MB,轻量易部署。已有116人学习下载,适合快速理解MVC分层结构、前后端交互流程与RBAC权限管理雏形。读者可直接运行查看完整购物流程(商品展示、购物车、订单管理)与后台CRUD操作,代码结构清晰、注释规范,配套LayUI前端框架与Layer弹窗组件,便于二次开发与技术迁移学习。
1. 为什么一个“带管理平台”的SSM商城项目,至今仍是Java初学者破局的关键跳板?
你可能已经刷过几十个“Spring Boot电商Demo”,但真正卡住新手的,从来不是“怎么写购物车”,而是——当用户下单后库存没扣减、管理员改完商品价格前端不刷新、日志里满屏NullPointerException却找不到调用链源头。这类问题在纯Boot项目里被自动配置层层包裹,像黑匣子;而SSM(Spring + SpringMVC + MyBatis)项目像一把解剖刀:它强制你亲手配数据源、写Mapper XML、定义Controller映射、处理事务传播级别。这个.zip包之所以被某高校实验室持续三年用作实训基线,正因为它把“商城”这个业务场景,拆解成可触摸的12个模块:用户中心、商品分类树、订单状态机、后台权限RBAC、Redis缓存穿透防护点、MySQL主从读写分离配置位……它不教你怎么炫技,只问你:“<select>标签里resultMap和resultType到底该选哪个?为什么@Transactional加在Service层方法上,但DAO层抛异常时事务却不回滚?”——答案就藏在spring-mvc.xml第87行和applicationContext.xml第153行的嵌套配置里。适合刚学完JDBC和Servlet、想摆脱“Hello World式Spring Boot”的开发者,也适合需要快速搭建教学演示环境的某导师。
2. 从解压到登录后台:5分钟跑通最小可运行路径
2.1 解压与目录结构认知:先看清“骨架”再动刀
下载解压后,你会看到标准Maven结构,但关键不在src/main/java,而在根目录下三个被新手忽略的配置枢纽:
├── src/ ├── target/ ├── pom.xml # 注意:Spring版本锁定在4.3.28.RELEASE,MyBatis为3.4.6 ├── webapp/ # JSP页面全在这里,非Thymeleaf │ ├── WEB-INF/ │ │ ├── web.xml # Servlet 3.0规范,注意<load-on-startup>值 │ │ └── spring-mvc.xml # Controller层核心,扫描包路径是com.mall.web │ └── static/ # CSS/JS放这里,非resources/static ├── resources/ # 配置文件集中地 │ ├── db.properties # 数据库连接参数,明文存储(教学用途) │ ├── applicationContext.xml # Service+DAO层整合,事务管理器在此声明 │ └── mybatis-config.xml # MyBatis全局配置,注意<typeAliases>包路径提示:
web.xml中<servlet>节点的<init-param>指定了SpringMVC配置文件路径,若后续报404,第一反应不是代码错,而是检查这个路径是否指向spring-mvc.xml真实位置。
2.2 数据库初始化:避开字符集与引擎的双重陷阱
项目SQL脚本位于src/main/resources/sql/mall_db.sql。执行前必须确认三件事:
- MySQL版本兼容性:脚本使用
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4,若你的MySQL < 5.7.7,需手动将utf8mb4替换为utf8(否则建表失败); - root密码策略:脚本含
CREATE USER 'mall_user'@'localhost' IDENTIFIED BY 'mall123';,若MySQL 8.0+启用caching_sha2_password插件,需在db.properties中追加&serverTimezone=Asia/Shanghai; - 外键约束:部分表含
FOREIGN KEY (category_id) REFERENCES t_category(id),若MySQL禁用外键(SET FOREIGN_KEY_CHECKS=0;),需在导入前手动开启。
执行命令(Linux/macOS):
mysql -u root -p < src/main/resources/sql/mall_db.sql成功后验证:SELECT COUNT(*) FROM t_user;应返回10条测试用户数据。
2.3 IDEA部署:绕过Tomcat 9的Servlet版本冲突
项目web.xml声明<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="3.0">,但IDEA默认Tomcat 9.0.83要求Servlet 4.0。解决方案:
- 在IDEA中打开Project Structure → Artifacts,确认Output Layout中
WEB-INF/lib已包含所有jar(尤其spring-webmvc-4.3.28.RELEASE.jar); - 进入Run → Edit Configurations → Tomcat Server → Deployment,点击
+号选择Artifact → mall:war exploded; - 关键一步:在Server选项卡中,将
JRE改为项目SDK(如1.8.0_291),取消勾选After launch下的Open in browser(避免因未启动Redis导致首页白屏); - 启动后访问
http://localhost:8080/mall/,首页应显示“欢迎来到线上商城”。
参数说明:
mall:war exploded表示以解压模式部署,修改JSP无需重启;若选mall:war,每次改页面都要重新打包,效率极低。
3. 管理平台登录失效?三个核心配置点决定权限系统生死
3.1 Shiro权限框架的三重拦截链:从URL到方法级控制
项目采用Apache Shiro而非Spring Security,因其配置更直观。权限控制分三层:
| 层级 | 配置文件 | 控制粒度 | 典型配置项 |
|---|---|---|---|
| URL级 | shiro.ini(位于resources/) | /admin/** = authc, roles[admin] | 拦截所有/admin路径,要求登录且角色为admin |
| 方法级 | @RequiresPermissions("user:delete") | Service层方法注解 | 需在applicationContext.xml中启用<aop:config> |
| 页面级 | JSP中<shiro:hasRole name="admin"> | JSP标签库 | 需在web.xml中注册ShiroFilter |
逻辑说明:用户访问
/admin/user/list时,ShiroFilter先查shiro.ini匹配URL规则,通过后进入Controller,Controller调用Service时,@RequiresPermissions触发AOP代理校验权限,最后JSP渲染时<shiro:hasRole>决定按钮是否显示。任一环缺失,都会导致“能进后台但看不到菜单”或“点了删除按钮没反应”。
3.2 登录凭证校验:为什么输入正确账号密码仍跳转到login.jsp?
根本原因在ShiroRealm.java的doGetAuthenticationInfo方法。常见错误有三:
- 密码加密方式不匹配:数据库中密码是
MD5(salt+password),但代码中误用new SimpleHash("SHA-256", password, salt); - 盐值获取错误:
ByteSource.Util.bytes(user.getCredentialsSalt())中user.getCredentialsSalt()返回null,因t_user表credentials_salt字段为空; - Realm未注入Spring容器:
applicationContext.xml中<bean id="shiroRealm" class="com.mall.shiro.ShiroRealm">缺少<property name="userService" ref="userService"/>依赖注入。
修复步骤:
// ShiroRealm.java 第42行 // 错误写法(硬编码盐值) SimpleAuthenticationInfo info = new SimpleAuthenticationInfo( user, user.getPassword(), ByteSource.Util.bytes("123456"), getName()); // 正确写法(从DB取盐值) String salt = user.getCredentialsSalt(); if (salt == null || salt.trim().length() == 0) { salt = "mall_default_salt"; // 降级方案 } SimpleAuthenticationInfo info = new SimpleAuthenticationInfo( user, user.getPassword(), ByteSource.Util.bytes(salt), getName());参数说明:
ByteSource.Util.bytes(salt)将字符串转为字节数组,Shiro内部用此生成MD5哈希;若传入null会抛NullPointerException,但日志只打印Authentication failed for token submission,需断点调试doGetAuthenticationInfo。
3.3 角色与权限数据加载:后台菜单为何始终显示“暂无数据”?
问题根源在MenuService.java的findMenuByRoleId方法。该方法从t_menu和t_role_menu联查,但t_role_menu表初始为空。教学包中预置了role_id=1(管理员)对应菜单ID,但未提供初始化SQL。
手动补全SQL(执行一次):
INSERT INTO t_role_menu(role_id, menu_id) VALUES (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8); -- 对应菜单:首页、用户管理、商品管理、订单管理、分类管理、规格管理、广告管理、系统设置血泪经验:某开发者曾花3小时排查,最终发现
t_role_menu表为空,而日志中SELECT * FROM t_role_menu WHERE role_id=?返回空集合,但Service层未做空集合判空,直接返回null,导致前端JS解析失败。建议在MenuService中添加:if (menuList == null || menuList.isEmpty()) { return Collections.emptyList(); // 防止NPE }
4. 避坑指南:SSM商城项目中90%新手栽在以下5个具体位置
4.1 现象:首页轮播图不显示,浏览器F12看Network全是404
原因:webapp/static/images/banner/路径下图片文件名与t_ad表中img_url字段值不一致。例如数据库存banner1.jpg,但实际文件是banner1.jpeg(扩展名大小写敏感)。
解决:统一用小写扩展名,并在AdServiceImpl.java中getBannerList()方法里添加日志:log.info("Loading banner image: {}", ad.getImgUrl());,对比文件系统路径。
4.2 现象:添加商品时上传图片,后台报org.springframework.web.multipart.MultipartException: Current request is not a multipart request
原因:spring-mvc.xml中<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">未启用,或web.xml中<filter>顺序错误导致请求未被解析。
解决:确认web.xml中CharacterEncodingFilter必须在ShiroFilter之前,且<filter-mapping>的<url-pattern>为/*;同时检查pom.xml是否引入commons-fileupload依赖(版本需与Spring 4.3兼容,推荐1.3.3)。
4.3 现象:订单支付状态更新后,用户中心订单列表仍显示“待支付”
原因:OrderServiceImpl.java中updateOrderStatus方法使用@Transactional,但调用方OrderController未通过Spring代理调用(即this.updateOrderStatus()),导致事务失效。
解决:将方法改为public void updateOrderStatus(Long orderId, Integer status),并在Controller中@Autowired private OrderService orderService;后调用orderService.updateOrderStatus(...),严禁this.updateOrderStatus(...)。
4.4 现象:Redis缓存商品详情,但修改商品后前端仍显示旧价格
原因:ProductServiceImpl.java中updateProduct方法更新数据库后,未同步删除Redis中product:{id}缓存,且@CacheEvict注解未生效(因Shiro代理与Spring AOP代理冲突)。
解决:弃用注解,改用手动操作:
// 更新数据库后 redisTemplate.delete("product:" + product.getId()); // 并确保redisTemplate bean在applicationContext.xml中已声明4.5 现象:Tomcat启动时报java.lang.ClassNotFoundException: org.springframework.web.context.ContextLoaderListener
原因:pom.xml中spring-web依赖范围为<scope>test</scope>,或IDEA未将Maven依赖导出到WEB-INF/lib。
解决:检查pom.xml,确保spring-web无<scope>标签;在IDEA中右键项目 →Maven → Reimport,然后进入Project Structure → Artifacts,确认Available Elements中Maven: org.springframework:spring-web:4.3.28.RELEASE已拖入WEB-INF/lib。
5. 让管理平台真正可用:三个必须动手改造的实战技巧
5.1 给商品列表加搜索过滤:从静态SQL到动态条件拼接
原ProductMapper.xml中查询语句为:
<select id="findProductList" resultType="Product"> SELECT * FROM t_product ORDER BY create_time DESC </select>无法按名称/分类筛选。改造为动态SQL:
<select id="findProductList" resultType="Product"> SELECT * FROM t_product <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY create_time DESC </select>对应ProductService.java中方法签名改为:
List<Product> findProductList(@Param("name") String name, @Param("categoryId") Long categoryId, @Param("status") Integer status);参数说明:
@Param注解让MyBatis识别多个参数;<where>标签自动处理AND开头的SQL拼接,避免手写WHERE 1=1;CONCAT('%', #{name}, '%')防止SQL注入,比'%${name}%'安全。
5.2 后台操作日志:用AOP记录谁在何时改了什么
项目缺操作审计功能。新建LogAspect.java:
@Aspect @Component public class LogAspect { @Autowired private LogService logService; @Around("@annotation(org.springframework.web.bind.annotation.PostMapping) || " + "@annotation(org.springframework.web.bind.annotation.PutMapping)") public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); Object result = joinPoint.proceed(); long cost = System.currentTimeMillis() - start; // 提取当前用户(从Shiro获取) Subject subject = SecurityUtils.getSubject(); String username = subject.getPrincipal() != null ? subject.getPrincipal().toString() : "anonymous"; // 记录日志 Log log = new Log(); log.setOperator(username); log.setOperation(joinPoint.getSignature().getName()); log.setCostTime(cost); log.setCreateTime(new Date()); logService.save(log); return result; } }需在applicationContext.xml中启用AOP:
<aop:aspectj-autoproxy /> <bean id="logAspect" class="com.mall.aspect.LogAspect" />技巧价值:某公司上线后发现商品价格被恶意篡改,靠此日志5分钟定位到操作人IP和时间,比翻数据库binlog快10倍。
5.3 前端防重复提交:给订单提交按钮加Token机制
原order.jsp中表单直接提交,易造成重复下单。改造步骤:
- 在
OrderController.java中生成Token:
@GetMapping("/create") public String createOrder(Model model) { String token = UUID.randomUUID().toString(); redisTemplate.opsForValue().set("order_token:" + token, "valid", 5, TimeUnit.MINUTES); model.addAttribute("token", token); return "order/create"; }- JSP中加入隐藏域:
<form action="${pageContext.request.contextPath}/order/submit" method="post"> <input type="hidden" name="token" value="${token}" /> <!-- 其他字段 --> </form>OrderController.submit()中校验:
@PostMapping("/submit") @ResponseBody public Result submitOrder(@RequestParam String token) { String cacheToken = redisTemplate.opsForValue().get("order_token:" + token); if (cacheToken == null) { return Result.fail("重复提交,请刷新页面"); } redisTemplate.delete("order_token:" + token); // 消费Token // 执行下单逻辑 }避坑提醒:Token必须设5分钟过期,否则用户填完表单去付款,回来提交时Token已失效;Redis key命名必须带前缀
order_token:,避免与其他模块冲突。
我带过的某高校实训班,第三周作业就是给这个商城加Token防重提。有学生第一次实现时忘了redisTemplate.delete(),导致Token永远有效,被同学用脚本狂刷100单——这恰恰是最好的教学案例:安全不是加个注解,而是理解状态生命周期。希望帮到你。
本文还有配套的精品资源,点击获取