
简介这是一套基于SSM框架开发的购物商城系统完整源码适合正在做Java Web课程设计、毕业设计或想学习SSM整合开发的读者。系统包含前台商城展示与后台管理功能前后台结构完整。压缩包内共有2000个文件大小约66.94MB其中以Java源码、class编译文件、JSP/HTML页面、JS与CSS前端资源为主同时包含XML配置文件、数据库SQL脚本、导入工具及说明文档方便对照学习前后端交互与SSM配置。运行前需将JDK、Tomcat调整为本地版本并修改数据库连接信息注意代码中有两处配置即可在本地部署体验。目前已有140人学习下载适合具备一定Java基础、希望快速获得一个可运行商城项目作为参考的开发者。1. 从 .rar 解压出来的 SSM 购物商城系统先看懂框架再跑业务在技术社区的帖子、培训机构备份盘和企业内部交接文档里几乎都能翻到一个命名为“ssm框架购物商城系统.rar”的压缩包。解压出来就是一套典型 Java Web 工程Spring 管理业务对象、Spring MVC 接收请求并分发、MyBatis 负责数据库访问。这套组合虽然已不如 Spring Boot 新但在中小型电商系统、教育系统和大量内部管理后台里SSM 依然是实打实的技术底子。对刚接触框架的人商城是最直观的三层架构样例对维护老系统的人理解这套结构直接决定排障效率。不同来源压缩包内容差异不小有的带 SQL 脚本有的只有源码我按业内最标准工程形态讲操作路径下载后看什么、配置改哪里、核心链路怎么写、部署后排查哪些坑。你手头解压出来的目录以它为准下文所有路径和包名都是最通用的写法。2. SSM 框架在购物商城的工程分工先把三层架构理清刚拿到压缩包时先去分清楚 controller、service、dao 三个包的边界再谈配置。SSM 不是一个大框架而是 Spring、Spring MVC、MyBatis 三个框架各管一段Spring 管对象实例和事务Spring MVC 管 HTTP 请求的接入和响应MyBatis 管 SQL。商城里“用户下单”这个动作从页面点击到库表写入要依次穿过这三层每一层只做自己的事层与层之间靠接口隔开。下面按分层职责逐个拆。2.1 Spring 的 IOC 容器和 AOP 事务是商城服务层的地基Spring 在 SSM 工程里承担两件事其一是 IOCservice 实现类不再自己 new而是交给容器实例化业务代码声明字段容器按类型注入其二是 AOP把开启事务、提交、回滚这类横切逻辑统一加到方法前后。商城的下单、扣库存、支付回调都依赖强一致事务这两块缺一不可。SSM 项目里的事务配置通常在单独 XML 文件中bean idtxManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:advice idtxAdvice transaction-managertxManager tx:attributes tx:method namecreate* propagationREQUIRED/ tx:method nameupdate* propagationREQUIRED/ tx:method namedelete* propagationREQUIRED/ /tx:attributes /tx:advice aop:config aop:advisor advice-reftxAdvice pointcutexecution(* com.shop.service.*.*(..))/ /aop:configREQUIRED表示当前没有事务就新建已有事务就加入适合 create/update/delete 开头的方法。有的事务配置会把select*标成read-onlytrue实际读操作不用事务加上只读反而可能让 MySQL 走旧快照产生重复读问题所以我不建议给查询方法强行包只读事务。这里最隐蔽的问题是pointcut只匹配com.shop.service包下一级类如果实现类在com.shop.service.impl子包里切点就落空表现为下单异常时不回滚——这是后面第 5 章要重点排查的现场。2.2 Spring MVC 的 DispatcherServlet 与商城 URL 路由映射Spring MVC 的核心是 DispatcherServlet所有请求先到它这里再由 HandlerMapping 找到对应 Controller 方法执行后返回视图名。商城项目里的路由有很强规律商品列表是/product/list购物车是/cart/list提交订单是/order/save登录是/user/login。如果解压出来的 Controller 里/admin/**和/cart/**混在一个类里说明作者没有按模块拆路由维护时改一个接口容易碰到另一个。登录成功后的跳转是这套路由里比较典型的一段Controller RequestMapping(/user) public class UserController { Autowired private UserService userService; RequestMapping(value /login, method RequestMethod.POST) public String login(String username, String password, HttpSession session, Model model) { User user userService.login(username, password); if (user null) { model.addAttribute(msg, 用户名或密码错误); return user/login; } session.setAttribute(loginUser, user); return redirect:/product/list; } }RequestMapping(value /login, method RequestMethod.POST)限定只接 POST 请求避免用户把密码拼在 URL 上。redirect:/product/list是重定向写法浏览器地址会变成商品列表同时刷新 Session 中的用户状态。这里有个容易被忽略的细节如果 Controller 里既有/login的 GET 又写 POST两个方法必须用 method 区分开否则启动报重复映射异常。2.3 MyBatis 把 SQL 放在 Mapper商城查询和分页才好维护MyBatis 的特点是把 SQL 写在 XML 里由 Mapper 接口和 XML 做一一映射。商城项目最典型的场景是商品检索按分类查、按关键词模糊查、按价格区间查再分页。把所有过滤条件写进 XML用动态 SQL 拼接比在 Java 里用字符串拼 SQL 安全得多页面传什么参数就拼什么条件。select idpageQuery resultTypecom.shop.entity.Product SELECT id, name, price, stock, category_id, image, create_time FROM product where if testcategoryId ! null AND category_id #{categoryId} /if if testkeyword ! null and keyword ! AND name LIKE CONCAT(%, #{keyword}, %) /if if testminPrice ! null AND price ![CDATA[ ]] #{minPrice} /if /where ORDER BY create_time DESC /selectwhere会自动去掉第一个条件前面多余的 AND#{categoryId}是预编译占位符能挡住 SQL 注入比${}拼接安全得多。CONCAT(%, #{keyword}, %)是标准模糊查询写法不推荐直接用if testkeyword ! nullLIKE %${keyword}%/if那等于把用户输入直接拼进 SQL。价格条件里的大于等于号用 CDATA 包住避免 XML 转义出问题。这类文件如果启动时报 “Content is not allowed in prolog”多半是文件头部有 BOM 或声明写错。2.4 为什么老商城还在用 SSM以及什么时候该迁移现在新项目都用 Spring Boot但存量 SSM 商城还很多。跑得好好的系统没人愿意为换技术而重构SSM 三层边界清晰出了问题能直接在 filter、controller、service、mapper 四层里定位。Spring Boot 的自动配置在做一个定制程度很高的商城时反而要不断排除自动装配项迁移成本不低。对比项SSM 商城Spring Boot 商城配置方式XML 注解混合约定优先注解为主部署形态war 包丢 Tomcat内置容器jar 直接跑事务管理声明式事务 AOP 配置Transactional 默认生效上手曲线要先懂容器原理快但排障偏黑盒迁移时机一般出现在两个节点商城要接入大量第三方 SDK或者团队决定拆微服务。前者 Boot 的自动装配更省力后者 SSM 的 war 包在容器编排里部署繁琐。除此之外就让它在现有 Tomcat 上稳定跑着别把一个正在盈利的系统当重构试验田。3. 拿到 ssm 购物商城压缩包后的三个动作导工程、建库、改配置解压后的第一件事不是急着启动 IDEA。我见过太多人连数据库表都没建就导入工程然后被一堆红色报错吓到。正确顺序是固定的确认 Maven 依赖拉得下来、把 SQL 导进数据库、检查并修正配置。顺序不能乱因为 Spring 容器启动时就要初始化数据源数据源连不上后面全是连环报错。3.1 工程结构、Maven 依赖和启动入口标准 SSM 商城源码包解压后目录基本是这副骨架root ├── pom.xml ├── src/main/java │ └── com/shop │ ├── controller │ ├── service │ ├── dao │ └── entity ├── src/main/resources │ ├── applicationContext.xml │ ├── spring-mvc.xml │ ├── mybatis-config.xml │ ├── jdbc.properties │ └── mapper └── src/main/webapp ├── WEB-INF/web.xml └── static如果压缩包里没有 pom.xml只有 .class 文件或整个.idea目录说明这不是源码工程可执行的改动空间很小。以 pom.xml 作为工程入口用 IDEA 的 Open 选择该目录IDE 识别成 Maven 项目后会自动拉依赖。pom 里至少要出现这五个坐标spring-webmvc、spring-jdbc、mybatis、mybatis-spring、mysql-connector-java再加 javax.servlet-api 和 jstl 就够跑通。某些包会直接用 DruidDataSource 替代 DBCP这时 resources 下还会多一个 druid.properties这是好事Druid 自带监控页面排查连接泄漏时有用。3.2 商城关系型数据模型SQL 脚本导入的正确姿势商城数据库最小也得六张表用户、分类、商品、购物车、订单、订单明细。多数 SSM 源码包会把 SQL 放在resources/sql/或压缩包根目录的db/文件夹里文件名形如mall.sql或store.sql。如果包里没有脚本就需要根据 entity 类和 mapper XML 反推建表语句工作量不小这类包一般是只给了部分源码不建议硬啃。六张表的职责划分如下表名作用主要字段user用户账号id, username, password, nickname, phonecategory商品分类id, name, sortproduct商品主体id, category_id, name, price, stockcart购物车行id, user_id, product_id, quantityorders订单主表id, user_id, order_no, total_price, statusorder_item订单明细id, order_id, product_id, quantity, price把 SQL 文件导入 MySQLmysql -uroot -p shopping /path/to/shopping.sqlshopping是目标库名SQL 文件里如果没有CREATE DATABASE语句就得先手动建库。导入后立刻验证两条语句SELECT COUNT(*) FROM product; SELECT COUNT(*) FROM user;商品表和用户表的 count 都大于 0说明数据初始化成功。如果 product 表为 0后面商品列表页会空白查 log 也看不到业务报错。注意orders 里的order是 MySQL 保留字表名写成orders也可能在某些版本触发语法错误。稳妥做法是建表时叫t_order或统一加mall_前缀。源码包里的表名是写死的改表名要连 mapper XML 一起改所以下载包里如果是orders建议确认 MySQL 版本后决定要不要动。3.3 数据库连接参数、上下文路径、JDK 版本解压必改的三处改配置的第一优先级是数据库连接集中在jdbc.propertiesjdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://127.0.0.1:3306/shopping?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 jdbc.usernameroot jdbc.password123456MySQL 5.7 用com.mysql.jdbc.Driver没问题MySQL 8.0 必须换成com.mysql.cj.jdbc.Driver并且要加serverTimezoneAsia/Shanghai否则启动直接抛The server time zone value异常。characterEncodingutf8少了中文商品名在页面显示乱码。这里的密码不要留生产库的真实凭据练习环境单独建一个低权限账号更符合习惯。第二处是上下文路径。Tomcat 部署后访问地址是http://localhost:8080/工程名/JSP 里如果用绝对路径写死了/ssm-shop/工程名一改页面就 404。正规写法是在 JSP 头部用${pageContext.request.contextPath}拼静态资源路径遇到用死路径写link的旧代码要全局替换成动态上下文。第三处是 JDK 版本。SSM 工程大多出生在 JDK 8 时代本地装 JDK 17 而不动 pom.xmlMaven 默认按新版本编译会直接报错。在 pom 的 properties 里固定编译版本properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties编译级别对齐到 1.8Tomcat 也用 8.5 系列兼容性最稳妥。这三处处理完再启动大部分“起不来”的问题就能绕过去。3.4 在 IDEA Tomcat 里启动验证第一个页面配置全部就位后IDEA 里添加 Tomcat ServerDeployment 选 war explodedApplication context 填根路径/。这样启动不需要整体打包改 JSP 还能热更新。点启动前先确认本地 8080 端口没被占用lsof -i :8080占用就换 8081Tomcat 端口改 server.xml不然后端一直起不来。启动完成后用 curl 验证curl -I http://localhost:8080/index.jsp返回HTTP/1.1 200说明 Tomcat 和工程都起来了。如果首页本身就是登录页浏览器打开后能看到登录框下一步就可以进第 4 章的代码链路阅读。4. 登录拦截、商品分页、购物车与下单SSM 商城的核心链路实现工程能跑开始读核心代码。商城业务主干按“登录 → 浏览商品 → 加入购物车 → 结算下单 → 扣库存”展开。每条链路都横跨三层下面按业务顺序给出实现要点每一处代码都可以直接对着你解压出来的包比对。4.1 登录状态怎么维持拦截器保护哪些 URL登录成功后多数 SSM 商城把 User 对象放进 Session之后每个请求都从 Session 里取。Controller 层拿到用户名密码去 service 校验成功就 set 进 session失败回登录页并带提示。Session 是默认方案不引入 Redis 的原因也简单老商城架构里Session 丢失不外乎服务重启单体应用重启频率低用数据库或 Redis 维护会话反而增加链路。RequestMapping(/login) public String login(String username, String password, HttpSession session, Model model) { User user userService.login(username, password); if (user null) { model.addAttribute(msg, 用户名或密码错误); return user/login; } session.setAttribute(loginUser, user); return redirect:/product/list; }这里需要特别留意的业务字段是service 层做登录校验时密码比对应该是 MD5 加盐或 BCrypt。很多练习项目直接明文比较放到公网等于裸奔只要数据库一泄露所有账号跟着完。受保护接口用拦截器统一拦截而不是在每个 Controller 里重复判断 session。自定义一个 LoginInterceptorpublic class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object loginUser request.getSession().getAttribute(loginUser); if (loginUser null) { response.sendRedirect(request.getContextPath() /user/login); return false; } return true; } }在 spring-mvc.xml 里注册拦截路径mvc:interceptors mvc:interceptor mvc:mapping path/order/**/ mvc:mapping path/cart/**/ mvc:exclude-mapping path/cart/list/ bean classcom.shop.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptorsexclude-mapping不加的话游客连购物车列表都看不了连点“加入购物车”都会被踢回登录页这就过了。下单、支付、后台管理这三类接口必须拦截商品列表和详情要放行这是商城权限设计的基本盘。4.2 商品列表的分页PageHelper 和手写 LIMIT 怎么选商品列表最直接的需求是分页。常见做法两种引入pagehelper依赖查询前调用PageHelper.startPage(pageNum, pageSize)或者手写 mapper XML在 SQL 末尾拼接LIMIT #{offset}, #{pageSize}。PageHelper 的优点是业务代码不用改 SQL一个静态方法解决缺点是复杂 join 里插件动 SQL 容易出错排查时得睁大眼睛看生成的 SQL。public PageInfoProduct pageQuery(Integer pageNum, Integer pageSize, ProductQuery query) { PageHelper.startPage(pageNum, pageSize); ListProduct list productMapper.pageQuery(query); return new PageInfo(list); }PageHelper.startPage只对紧随其后的第一条 SQL 生效不能提取成公共变量提前调用。PageInfo里带pageNum、pageSize、total、pages、list等字段JSP 直接迭代page.list。如果发现没分页先检查 startPage 和 select 之间是不是插了别的 mapper 调用一旦有插件会把分页加到那张表上结果全然不对。4.3 购物车存在 Session 还是数据库差别在登录后能否恢复这是 SSM 商城代码里最容易引发讨论的点。存 Session实现简单Controller 放一个 Mapkey 是 productIdvalue 是数量不落库浏览器一关数据就没了。存数据库每个用户一张 cart 表加购、改数量、删行都走 mapper换设备也能恢复但每次渲染购物车都要查库。我一般这样分练习项目用 Session把结构暴露得更清楚真实上线至少商品价格和 SKU 以数据库为准Session 只存 key 和数量页面渲染时再查库对比最新价格防止用户拿旧价格结算。SSM 商城最常见的 cart 表实现里加购方法长这样RequestMapping(/add) public String add(RequestParam Integer productId, RequestParam Integer quantity, HttpSession session) { User loginUser (User) session.getAttribute(loginUser); CartItem item new CartItem(); item.setUserId(loginUser.getId()); item.setProductId(productId); item.setQuantity(quantity); cartService.add(item); return redirect:/cart/list; }RequestParam默认参数必传缺少 productId 直接 400前端调用时要保证字段名一致。真正容易漏的是业务校验商品是否在售、quantity 是否超过库存上限。不少商城项目直接 insert 进购物车不校验库存等下单减库存时才发现负库存属于业务层缺陷到并发压测才会暴露。4.4 下单闭环的事务边界扣库存为什么必须用条件 UPDATE订单创建牵涉三张表写入顺序我习惯固定为订单主表 → 订单明细 → 扣减库存 → 清空购物车。后一步失败前面所有操作都要回滚所以这四个动作必须包在同一个事务里Transactional直接标注在 service 实现类的方法上。Override Transactional(rollbackFor Exception.class) public Long createOrder(Long userId, ListCartItem cartItems) { Order order new Order(); order.setUserId(userId); order.setOrderNo(UUID.randomUUID().toString().replace(-, )); order.setStatus(0); order.setCreateTime(new Date()); orderMapper.insert(order); for (CartItem item : cartItems) { OrderItem oi new OrderItem(); oi.setOrderId(order.getId()); oi.setProductId(item.getProductId()); oi.setQuantity(item.getQuantity()); orderItemMapper.insert(oi); int rows productMapper.decreaseStock(item.getProductId(), item.getQuantity()); if (rows 0) { throw new StockNotEnoughException(库存不足); } } cartMapper.deleteByUserId(userId); return order.getId(); }仔细看扣库存那句它必须是一个带条件的 UPDATE 而不是先查再改UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}stock #{quantity}是并发防超卖的关键。两个请求同时进来抢同一件库存为 5 的商品A 买 4B 买 2MySQL 的 UPDATE 行锁保证同一时刻只有一个请求能执行成功。A 更新完库存剩 1B 再执行时条件stock 2不成立影响行数是 0service 层捕获到 rows 0 后抛出异常事务回滚订单主表和明细都不落库。这个写法是商城防超卖的底线方案任何绕开它的方案在并发下都会出问题。5. 把 SSM 商城打成 war 部署到 Tomcat再把三个坑扫一遍源码包最终通常要打成 war 交给运维。下面按部署顺序走一遍每一步对应一个可验证的结果卡住的地方按技术点排查。5.1 Maven 打包命令和 Tomcat 部署方式先清后包避免 stale class 干扰结果mvn clean package -DskipTests跳过测试打包避免单元测试环境不完整导致构建中断。成功后target/目录生成xxx.war文件名决定了应用访问上下文。放进 Tomcat 的webapps/重启/opt/tomcat/bin/shutdown.sh /opt/tomcat/bin/startup.shTomcat 第一次启动会自动解压 war。部署完盯日志tail -f /opt/tomcat/logs/catalina.out看到Deploying web application archive和has finished两行Web 应用加载完成。war 包如果要改上下文路径直接重命名 war 文件即可Tomcat 按文件名生成 context。5.2 用 curl 验证核心接口是否接通部署完用 curl 做四个最小验证curl -I http://localhost:8080/ssm-shop/index.jsp curl -I http://localhost:8080/ssm-shop/user/login curl -I http://localhost:8080/ssm-shop/product/list curl -I http://localhost:8080/ssm-shop/cart/listindex.jsp和login返回 200 说明 Tomcat 与 MVC 链路通cart/list如果 302 跳转登录页属于正常拦截逻辑。关键是/product/list它要查数据库并渲染 JSP返回 500 说明 mapper XML 路径错或数据表没导全。再核对页面内容curl -s http://localhost:8080/ssm-shop/login | grep -o 登录能输出“登录”二字说明 JSTL 标签库和静态资源引用都好着。5.3 字符集、依赖缺失、事务切点部署后最常见的三个坑第一是中文乱码。Tomcat 老版本默认 URIEncoding 按 ISO-8859-1 解码URL 里的中文参数会乱要在server.xml的 Connector 上加URIEncodingUTF-8同时保证jdbc.url带characterEncodingutf8。两边缺一商品名或收货地址就会变成乱码页面和数据库各错一半不好查。第二是运行时缺依赖。压缩包里的 pom.xml 有时只写了 Spring 和 MyBatis漏掉 jstl 或 commons-fileupload启动后页面正常但一访问 JSP 就 500日志报NoClassDefFoundError。按报错的类名去 mvnrepository 搜坐标补到 pom 里重新打包。第三是事务不生效这是最隐蔽的。日志里看不到Creating new transaction信息说明 Spring AOP 的切点没拦住 service 实现类。看到包名是com.shop.service.impl而事务 pointcut 写的是execution(* com.shop.service.*.*(..))就立刻能定位它只匹配service下一级类处理impl子包需要把切点改成com.shop.service..*.*(..)。事务失效的经典现象是下单异常后订单主表还在、明细缺一半出现这个现场先看事务日志别急着改业务代码。最后再提一个定位技巧把 MyBatis 的 SQL 日志输出打开会极大缩短定位时间。在 mybatis-config.xml 里加settings setting namelogImpl valueSTDOUT_LOGGING/ /settings生产环境的连接池 maxActive 至少调到 20验证并发后把它关掉回到logImpl为 Slf4j 的正常输出。SQL 日志打开时每一次 update 影响的行数直接印在控制台影响行数为 0 时再往回看 whether 事务切点与 SQL 条件是否匹配这比加断点调试高效得多。本文还有配套的精品资源点击获取