SSM商城项目实战:Java初学者掌握Spring+MyBatis核心配置的跳板
简介这是一套基于SSMSpringSpringMVCMyBatis框架与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项目里被自动配置层层包裹像黑匣子而SSMSpring 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.RELEASEMyBatis为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 # ServiceDAO层整合事务管理器在此声明 │ └── 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版本兼容性脚本使用ENGINEInnoDB DEFAULT CHARSETutf8mb4若你的MySQL 5.7.7需手动将utf8mb4替换为utf8否则建表失败root密码策略脚本含CREATE USER mall_userlocalhost IDENTIFIED BY mall123;若MySQL 8.0启用caching_sha2_password插件需在db.properties中追加serverTimezoneAsia/Shanghai外键约束部分表含FOREIGN KEY (category_id) REFERENCES t_category(id)若MySQL禁用外键SET FOREIGN_KEY_CHECKS0;需在导入前手动开启。执行命令Linux/macOSmysql -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 xmlnshttp://xmlns.jcp.org/xml/ns/javaee version3.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 nameadminJSP标签库需在web.xml中注册ShiroFilter逻辑说明用户访问/admin/user/list时ShiroFilter先查shiro.ini匹配URL规则通过后进入ControllerController调用Service时RequiresPermissions触发AOP代理校验权限最后JSP渲染时shiro:hasRole决定按钮是否显示。任一环缺失都会导致“能进后台但看不到菜单”或“点了删除按钮没反应”。3.2 登录凭证校验为什么输入正确账号密码仍跳转到login.jsp根本原因在ShiroRealm.java的doGetAuthenticationInfo方法。常见错误有三密码加密方式不匹配数据库中密码是MD5(saltpassword)但代码中误用new SimpleHash(SHA-256, password, salt)盐值获取错误ByteSource.Util.bytes(user.getCredentialsSalt())中user.getCredentialsSalt()返回null因t_user表credentials_salt字段为空Realm未注入Spring容器applicationContext.xml中bean idshiroRealm classcom.mall.shiro.ShiroRealm缺少property nameuserService refuserService/依赖注入。修复步骤// 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_id1管理员对应菜单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 idmultipartResolver classorg.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依赖范围为scopetest/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 idfindProductList resultTypeProduct SELECT * FROM t_product ORDER BY create_time DESC /select无法按名称/分类筛选。改造为动态SQLselect idfindProductList resultTypeProduct SELECT * FROM t_product where if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if if testcategoryId ! null AND category_id #{categoryId} /if if teststatus ! null AND status #{status} /if /where ORDER BY create_time DESC /select对应ProductService.java中方法签名改为ListProduct findProductList(Param(name) String name, Param(categoryId) Long categoryId, Param(status) Integer status);参数说明Param注解让MyBatis识别多个参数where标签自动处理AND开头的SQL拼接避免手写WHERE 11CONCAT(%, #{name}, %)防止SQL注入比%${name}%安全。5.2 后台操作日志用AOP记录谁在何时改了什么项目缺操作审计功能。新建LogAspect.javaAspect 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中启用AOPaop:aspectj-autoproxy / bean idlogAspect classcom.mall.aspect.LogAspect /技巧价值某公司上线后发现商品价格被恶意篡改靠此日志5分钟定位到操作人IP和时间比翻数据库binlog快10倍。5.3 前端防重复提交给订单提交按钮加Token机制原order.jsp中表单直接提交易造成重复下单。改造步骤在OrderController.java中生成TokenGetMapping(/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 methodpost input typehidden nametoken value${token} / !-- 其他字段 -- /formOrderController.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单——这恰恰是最好的教学案例安全不是加个注解而是理解状态生命周期。希望帮到你。本文还有配套的精品资源点击获取