ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SSM在线相机商城:JavaWeb核心框架整合实战指南

2026/10/7 22:12:15 拓冰建站 浏览量
SSM在线相机商城:JavaWeb核心框架整合实战指南 如果你正在做JavaWeb方向的毕业设计或者刚把SSM框架学完、想找一套完整系统动手练一练基于SSM在线相机商城这个选题值得你认真对待。这套技术组合是java ssm jsp jquery mysql项目本身并不算新但它把Spring、SpringMVC、MyBatis、JSP、jQuery、MySQL这几个JavaWeb领域最核心的技术栈全部串成了一条完整业务链用户登录注册、商品浏览、购物车、下单结算、后台商品管理。相比单纯的管理系统它多出了购物车和订单这条电商核心链路难度合适、业务完整用来交课程设计或者作为框架整合的练手项目都很合适。这篇内容我会从技术选型、数据库设计、SSM整合配置到购物车和订单的实现思路、JSP和jQuery的交互细节再到我实际踩过的那些坑全部梳理一遍。1. 为什么是“SSM JSP jQuery MySQL”这套组合1.1 SSM依然是最值得手动整合一遍的框架组合SSM是Spring、SpringMVC、MyBatis三件套的合称分工非常清晰Spring负责管理对象依赖和事务SpringMVC负责接收前端请求并分发到对应处理器MyBatis负责Java对象与数据库表之间的映射和SQL执行。三个框架各管一段刚好覆盖一次请求从浏览器到数据库再返回的完整链路。我见过不少同学直接上手Spring Boot做完整套商城却连Mapper、Service、Controller为什么分成三层都说不清楚。SSM这种组合的好处在于所有配置都是显式的你必须在spring-context.xml里手动注册数据源在spring-mvc.xml里手动配置视图解析器在mybatis-config.xml里手动开启驼峰映射。把这些配置亲手写一遍你会发现整个JavaWeb工程的运作方式变得特别通透。后面再去看Spring Boot的自动配置简直就像看老朋友一样亲切。1.2 JSP和jQuery在这里不是过时而是刚刚好视图层选JSP是因为课程设计和教学场景里它依然是最直观的方案。模型数据通过EL表达式和JSTL直接渲染到页面不需要额外搭建前端工程部署到Tomcat就能跑。jQuery负责补齐原生JavaScript在DOM操作和Ajax请求上的繁琐感实际开发体验比手写document.getElementById和XMLHttpRequest舒服太多。有些同学会纠结是不是要上Vue或者React我的建议很明确项目目标是完成一套逻辑完整的系统不是炫技术栈。jQuery的$.post、$.get、$.ajax足以覆盖商城项目里所有异步场景比如注册校验、购物车数量变更、加入购物车这些操作。把后端业务链路跑通、跑稳比堆一堆前端框架更能帮你理清系统设计的本质。1.3 MySQL版本选择直接决定你调试的时间成本数据库方面推荐直接用MySQL 5.7。我第一次做这类项目时身边有人装了MySQL 8.0驱动类名变成了com.mysql.cj.jdbc.Driver连接串里还要额外加serverTimezone参数光排查连接问题就折腾了两天。MySQL 5.7的驱动用com.mysql.jdbc.Driver连接串写成jdbc:mysql://localhost:3306/shop?useUnicodetruecharacterEncodingutf8useSSLfalse基本一遍就通。如果你确实已经装了8.0也能做但要把版本差异先搞清楚比如8.0的默认密码加密插件是caching_sha2_password老驱动连接时会报认证失败这类坑在网络上资料很多提前搜索能省不少时间。2. 数据库设计商城能不能跑稳七成看表2.1 六张核心表的建模思路我先把整个项目的表结构列出来一共六张用户表tb_user、分类表tb_category、商品表tb_product、购物车表tb_cart、订单表tb_order、订单明细表tb_order_item。这个结构基本是电商系统的最小完整闭环每一张表都有存在的必要。用户表tb_user字段有id、username、password、phone、email、avatar、create_time。密码不能明文存至少用MD5加密一次课程设计里MD5够用但你要知道生产环境应该用BCrypt这类更安全的哈希算法答辩时能说出这点是加分项。商品表tb_product字段有id、name、category_id、price、stock、main_image、description、status。这里有几个关键设计price必须用DECIMAL(10,2)千万不能用double或float否则页面上会出现一堆小数尾巴比如19.999999这种离谱数据status字段表示上下架状态1上架0下架下架商品在前端列表里不应该展示这比删除数据安全得多。main_image存的是相对路径比如/upload/xxx.jpg不存完整URL好处是以后换域名、换部署位置都不用改数据库页面渲染时用request.getContextPath()拼上前缀即可。购物车表tb_cart字段有id、user_id、product_id、quantity、create_time。为什么不直接用Session存购物车Session方案虽然实现很快但用户一关浏览器购物车就没了刷新后状态丢失也让人难受。存到数据库后用户登录随时能查到自己加了什么后台管理端也能看到全站购物车数据课程设计答辩时更有说服力。订单表tb_order字段有id、order_no、user_id、total_price、status、create_time、pay_time、consignee、phone、address。注意order_no是业务订单号页面和给用户看的都是这个号不要把数据库自增id暴露出来这是个习惯问题。订单状态我用数字表示1待付款、2待发货、3已发货、4已完成、5已取消后续用状态机管理。订单明细表tb_order_item字段有id、order_id、product_id、product_name、price、quantity。这张表必须冗余product_name和price因为商品信息以后可能会改但订单是历史快照用户查看历史订单时看到的应该是下单那一刻的商品名称和价格而不是现在的。这个设计细节很多初学者想不到但对真实电商系统来说非常重要。2.2 建表SQL和几个值得注意的细节以商品表和订单表为例核心建表SQL大概长这样DROP TABLE IF EXISTS tb_product; CREATE TABLE tb_product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, category_id INT NOT NULL, price DECIMAL(10, 2) NOT NULL, stock INT NOT NULL DEFAULT 0, main_image VARCHAR(255), description TEXT, status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8; DROP TABLE IF EXISTS tb_order; CREATE TABLE tb_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, total_price DECIMAL(10, 2) NOT NULL, status TINYINT DEFAULT 1, consignee VARCHAR(50) NOT NULL, phone VARCHAR(20) NOT NULL, address VARCHAR(255) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8;这里要提两个小坑。第一表名为什么要加tb_前缀因为order是MySQL的保留字直接建order表必然报语法错误加个前缀一劳永逸。第二每张表都加了ENGINEInnoDBInnoDB支持事务和外键商城系统涉及下单扣库存必须保证一致性MyISAM不支持事务千万别用。外键方面我的建议是尽量用逻辑外键也就是靠业务字段关联不建物理外键约束。物理外键在后续删除数据、批量导入时容易给你找麻烦但逻辑外键关联的字段一定要加索引比如tb_product的category_id、tb_order的user_id这些是高频查询条件索引能明显提升性能。初始数据插入几类相机就够用索尼、佳能、尼康、富士各放几款注意price保留两位小数stock给个合理数值比如10到50之间方便后面测试下单扣库存的效果。3. SSM整合实操从环境搭建到项目跑起来3.1 环境版本先确认省得后面连环报错我推荐的环境版本组合非常保守但稳定JDK 1.8、Maven 3.6及以上、Tomcat 8.5或9、MySQL 5.7、IDEA。这套组合在网上资料最多遇到报错基本都能搜到现成解决方案。JDK没必要上11或17一些老版本Tomcat和依赖在JDK 11以上反而会有兼容问题对于课程设计来说没有任何收益。Maven是必须的依赖管理全靠它。第一次做SSM整合的时候如果靠手动下载jar包往WEB-INF/lib里塞光是Spring、MyBatis、Jackson、JSTL这些依赖的版本匹配就能折腾一下午。用Maven管理pom.xml里写清楚版本一键下载。Tomcat版本就用8.5和JDK 1.8搭配最稳Servlet规范、JSP支持都和课程设计的需求完全匹配。3.2 项目目录结构规划一个好的目录结构能让你写代码时思路清晰。我是这样组织的src/main/java ├── com.shop.controller ├── com.shop.service ├── com.shop.service.impl ├── com.shop.mapper ├── com.shop.entity └── com.shop.util src/main/resources ├── jdbc.properties ├── mybatis-config.xml ├── spring-context.xml └── spring-mvc.xml src/main/webapp ├── static │ ├── css │ ├── js ├── upload ├── WEB-INF │ ├── jsp └── web.xmlentity里放和数据库表对应的实体类mapper里放MyBatis的接口和XML映射文件service写业务逻辑controller只负责接收请求和返回结果。注意service接口和impl实现类分开写这符合面向接口编程的习惯也是后面用Spring管理事务的基础。3.3 pom.xml核心依赖pom.xml里的依赖版本我用的是非常稳定的一套组合dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.2.22.RELEASE/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.9/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.6/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.47/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.9.8/version /dependency还需要加上数据库连接池依赖我用的是阿里Druid还有文件上传的commons-fileupload。版本选择的原则只有一个不要盲目追新课程设计阶段稳定优先。这些版本全是经过大量项目验证的老组合网上随便搜一个问题都能找到答案。3.4 三个核心配置文件的正确写法与作用spring-context.xml是Spring容器的主配置文件。我把业务层和持久层的扫描放这里但要把Controller排除掉因为Controller交给SpringMVC容器管理避免一个对象被初始化两次。context:component-scan base-packagecom.shop context:exclude-filter typeannotation expressionorg.springframework.stereotype.Controller/ /context:component-scan bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nameconfigLocation valueclasspath:mybatis-config.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.shop.mapper/ /bean bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/MapperScannerConfigurer的作用是扫描com.shop.mapper包下所有接口自动生成代理对象注入到Service中省掉手写DAO实现类的大量重复代码。spring-mvc.xml是SpringMVC子容器的配置重点有哪几个开启注解驱动、扫描controller包、配置视图解析器、放行静态资源。视图解析器配置了前缀/WEB-INF/jsp/和后缀.jsp这样Controller里返回字符串product/listSpringMVC会自动拼接成/WEB-INF/jsp/product/list.jsp。mvc:annotation-driven/ context:component-scan base-packagecom.shop.controller/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/jsp// property namesuffix value.jsp/ /bean mvc:resources mapping/static/** location/static// mvc:resources mapping/upload/** location/upload//web.xml里有三个关键配置缺一个都会出问题ContextLoaderListener负责加载Spring根容器DispatcherServlet负责拦截所有请求CharacterEncodingFilter负责编码统一。filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping这个filter的forceEncoding必须设置为true否则请求参数的中文和响应内容的中文会各乱一套。中文乱码问题十个有八个出在这个配置上后面排查章节会再展开。mybatis-config.xml里我强烈建议开启驼峰映射缓存mapUnderscoreToCamelCase开启后数据库字段user_name自动映射到Java属性userName省去手写大量resultMap的重复劳动。3.5 登录注册模块的完整链路注册功能的链路是JSP页面表单提交SpringMVC的UserController接收请求参数并封装成User对象调用UserService注册方法Service里对密码做MD5加密后调用UserMapper插入数据库最终返回JSON格式结果给前端。Controller的写法大概是这个风格Controller RequestMapping(/user) public class UserController { Autowired private UserService userService; PostMapping(/register) ResponseBody public Result register(User user) { String md5Pwd DigestUtils.md5DigestAsHex( user.getPassword().getBytes(UTF-8)); user.setPassword(md5Pwd); userService.register(user); return Result.success(); } }登录状态用Session保存登录成功之后把用户对象放进session。购物车加购、下单这些操作都要判断是否登录前端按钮可以判断一次但后端必须再判断一次前端判断只为了用户体验后端判断才是真正的安全边界。登录拦截器是必须写的继承HandlerInterceptor在preHandle方法里判断session里是否有user没有就重定向到登录页。注意拦截器要注册到SpringMVC配置中并且排除登录接口、注册接口、商品列表和详情这些不需要登录就能访问的路径。4. 购物车与下单商城业务的核心链路4.1 购物车方案选型Session还是数据库表购物车有两种常见方案我直接给出对比方案优点缺点适用场景Session存购物车实现快代码量少关闭浏览器丢失多设备不同步极简演示项目数据库购物车表持久化保存可追溯支持多端同步多写一套增删改查代码课程设计/真实项目做这个项目我建议直接上数据库方案。除了数据不丢失、答辩更好看之外还有一个实际好处后台管理可以查询全站用户的购物车数据这比Session方案带来的说服力强很多。购物车表在user_id上加索引因为最常用的查询就是通过user_id查当前用户的购物车列表。购物车条目的数量变更也是一个完整异步流程用户点击加减按钮jQuery发起Ajax请求后端Controller接收productId和新的quantity先判断这个数量不能小于1也不能超过库存然后更新数据库返回最新小计金额和购物车总数量给前端更新页面。这个交互很常规但很能体现细节处理能力。4.2 下单流程的完整时序与事务控制下单是整个项目最核心的操作涉及多张表写入必须放在一个事务里。完整的执行顺序是这样的用户从购物车页面点去结算跳转到订单确认页页面展示购物车商品列表和合计金额这个金额必须由后端根据数据库实时价格计算用户填写或确认收货人、手机号、地址信息提交订单后端开启事务依次执行生成订单号、计算总价、插入订单表、插入订单明细表、扣减库存、清空购物车任意一步失败全部回滚成功后跳转订单列表页服务层代码骨架如下Service public class OrderServiceImpl implements OrderService { Transactional(rollbackFor Exception.class) public Order submitOrder(Integer userId, String consignee, String phone, String address) { ListCart cartList cartMapper.selectByUserId(userId); BigDecimal total computeTotal(cartList); String orderNo generateOrderNo(userId); Order order new Order(orderNo, userId, total, consignee, phone, address); orderMapper.insert(order); for (Cart cart : cartList) { OrderItem item new OrderItem(order.getId(), cart.getProductId(), cart.getProductName(), cart.getProductPrice(), cart.getQuantity()); orderItemMapper.insert(item); int rows productMapper.deductStock( cart.getProductId(), cart.getQuantity()); if (rows 0) { throw new RuntimeException(库存不足); } } cartMapper.deleteByUserId(userId); return order; } }生成订单号我用的方案是时间戳加三位随机数yyyyMMddHHmmss格式的时间字符串加Random生成的三位数字查重后使用。这个方案简单可靠满足课程设计需求。这里有两个细节必须讲清楚。第一总价一定是在后端用数据库里的price乘quantity计算绝对不能信任前端传过来的金额任何前端传参都是可以被篡改的。第二扣库存的SQL必须加库存充足条件UPDATE tb_product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}如果受影响行数是0说明库存不够直接抛异常回滚。这个写法是为了防止超卖也就是多个用户同时买同一件商品时库存被扣成负数。很多人在答辩时被问到并发问题能答出这个点会非常加分。4.3 Transactional使用的三个关键注意点事务注解的坑比很多人想象的多。首先Transactional只对Spring管理的Bean方法生效必须加在Service实现类的方法上并且这个Service要被Spring扫描到。其次rollbackFor必须写成Exception.class因为Spring默认只在遇到RuntimeException时才回滚如果方法里抛的是SQLException这类检查异常事务不会回滚数据就会一半写入一半丢失。第三同一个类内部的调用不经过代理也就是说OrderServiceImpl里方法A直接调用方法B方法B上的Transactional不生效。所以下单核心逻辑要放在独立的Service方法里从Controller层通过注入的代理对象调用。订单状态机我需要简单定义一下1待付款、2待发货、3已发货、4已完成、5已取消。用户端在待付款状态可以取消订单后台管理员可以发货这两个操作都要校验当前状态避免状态乱跳。4.4 订单列表和越权访问的防范用户在个人中心查看订单列表时查询条件必须同时带上order_no和user_id。我见过一些项目只按order_no查询订单详情这等于把一个漏洞摆在那里用户只要循环遍历订单号就能看到别人的订单信息。正确的做法是Order order orderMapper.selectByOrderNoAndUserId(orderNo, userId);这种细节自己在写的时候容易被忽略但只要遇到过一次就会记住一辈子。作为课程设计项目答辩时主动说出“我做了越权校验”这句话评委老师的印象分马上就不一样。5. JSP jQuery那些容易被忽略的前端细节5.1 JSP页面公共结构组织商城页面有很多公共部分导航栏、顶栏购物车数量、登录状态、页脚每个页面都重复写一遍会非常痛苦。我用% include filecommon/header.jsp %把公共头部引入每个页面改动一次全部生效。注意这是静态include等于把文件内容直接嵌入公共变量可以共享适合导航这种场景。要是不同页面需要传递不同参数就用 jsp:include 动态引入。每个JSP页面第一行必须设置编码% page contentTypetext/html;charsetUTF-8 languagejava pageEncodingUTF-8 %这句话不写页面中文全部变成乱码这是零容忍错误。用到JSTL的页面记得加上taglib声明不然c:forEach这些标签全部报错。商品列表页的展示用c:forEach循环c:forEach items${pageBean.rows} varp div classproduct-card a href${pageContext.request.contextPath}/product/detail?id${p.id} img src${pageContext.request.contextPath}${p.mainImage}/ /a p${p.name}/p p${p.price}/p button onclickaddCart(${p.id})加入购物车/button /div /c:forEach这里有个超高频率踩坑点图片src绝对不能直接写/upload/xxx.jpg必须加上${pageContext.request.contextPath}前缀否则项目部署到带上下文的Tomcat路径下图片全部404。加上前缀后不管项目名怎么变路径都能正确解析。5.2 jQuery异步操作的核心场景jQuery在这个项目里要处理的异步操作大致有这些注册页面的用户名唯一性校验、登录表单提交、加入购物车、购物车数量加减、结算提交。我统一封装了一个Result对象包含code、message、data三个字段后端返回JSON前端统一判断code是否为200。这个约定能帮你省掉大量重复判断代码。用户名唯一性校验用blur事件$(#username).blur(function () { var username $(this).val(); if (username.length 3) { $(#tip).text(用户名至少3个字符); return; } $.get(ctx /user/checkUsername, {username: username}, function (res) { if (res.code 200) { $(#tip).text(用户名可用); } else { $(#tip).text(用户名已存在); } }, json); });加入购物车的完整交互是点击按钮后发送商品id到后端成功后更新顶栏购物车数量徽标function addCart(productId) { $.post(ctx /cart/add, {productId: productId}, function (res) { if (res.code 200) { $(#cartCount).text(res.data); } else if (res.code 401) { location.href ctx /user/login; } else { alert(res.message); } }, json); }表单提交用serialize()方法可以一次把所有表单字段拼好比如注册和登录页面直接$(#loginForm).serialize()传给后端省去一个个取值的重复劳动。这些技巧看着很基础但能把交互写得干净利落也比拼一堆前端框架实在得多。5.3 商品图片上传的后端处理后台添加商品时表单需要设置enctypemultipart/form-dataSpringMVC用MultipartFile类型接收上传文件。核心实现是把文件保存到项目部署目录下的upload文件夹再将相对路径存入数据库PostMapping(/admin/product/save) public String save(RequestParam(file) MultipartFile file, Product product) throws IOException { String originalName file.getOriginalFilename(); String suffix originalName.substring(originalName.lastIndexOf(.)); String fileName UUID.randomUUID().toString().replace(-, ) suffix; String uploadDir request.getSession().getServletContext() .getRealPath(/upload); file.transferTo(new File(uploadDir, fileName)); product.setMainImage(/upload/ fileName); productService.save(product); return redirect:/admin/product/list; }上传目录不能用写死的绝对路径项目换位置就废了。通过ServletContext.getRealPath获取部署路径是稳妥做法。文件名一定用UUID重命名避免用户上传的中文名文件造成乱码和覆盖。6. 常见问题与排查技巧6.1 项目启动失败类问题先给一张速查表这些是SSM项目最常见的启动期报错现象常见原因解决方向Tomcat启动失败端口被占用8080端口被其他进程占用Windows执行netstat -ano报CommunicationsExceptionMySQL服务没启动或连接参数错误检查mysql服务状态检查url、用户名密码报ClassNotFoundpom依赖缺失或版本错误检查对应artifactId是否在pom.xml中启动时找不到MapperMapper接口和XML不匹配检查namespace、方法id静态资源404DispatcherServlet拦了静态请求spring-mvc.xml加mvc:resources放行Tomcat端口被占用是新手必踩的坑。如果在IDEA里启动报Address already in use: JVM_Bind可以先看控制台提示的端口号一般就是8080。Windows下用命令netstat -ano | findstr 8080查占用进程的PID然后taskkill /PID 找到的PID /F强制结束或者直接把Tomcat的server.xml端口改掉。6.2 Invalid bound statement (not found) 的排查顺序这个报错几乎是MyBatis初学者必遇。它表示MyBatis找不到Mapper接口对应的SQL语句。排查顺序我建议固定为四步第一步看Mapper接口的全限定名和XML文件的namespace是否完全一致这是一个字母都不能差的第二步看XML文件里每个方法的id是否和接口方法名一致第三步确认XML文件在resources目录下的路径是否和namespace对应比如com.shop.mapper.ProductMapper接口的XML应该在resources/com/shop/mapper/ProductMapper.xml第四步检查SqlSessionFactoryBean是否配置了mapperLocations属性指向XML文件。排查这个错的时候不要靠猜就从第一条到第四条按顺序过一遍基本都能定位。6.3 中文乱码的定位顺序中文乱码出现的位置不同原因也不同。我总结的定位顺序是页面显示乱码先看JSP第一行是否设置了contentType和pageEncoding为UTF-8请求参数乱码就是后端拿到的中文变成问号看CharacterEncodingFilter有没有配置、forceEncoding是不是true响应JSON乱码看Controller的RequestMapping有没有指定producesapplication/json;charsetutf-8数据库存进去乱码看表是不是utf8、连接串有没有characterEncodingutf8。我实际遇到最多的情况是filter配置了但forceEncoding没写或者filter在web.xml里的位置不在最前面导致某些请求绕过了编码处理。最简单有效的办法就是严格按照我之前给的那个配置来写不要精简。6.4 上传后图片显示不出来的原因这个问题的坑点在于上传文件保存的目录有两种情况。如果你用ServletContext.getRealPath获取的路径文件保存到了Tomcat部署目录下的webapp里但IDEA热部署时文件可能被清理掉。解决思路是把上传目录放到Tomcat的外部目录然后通过Tomcat的虚拟目录映射访问或者干脆保存到项目webapp的upload目录并配置SpringMVC放行。排查时先确认当前项目实际部署路径下upload文件夹里有没有文件再用浏览器直接访问图片URL看返回什么状态码。404说明路径拼接有问题检查有没有带上下文路径。这个问题的答案往往就是contextPath没拼对。6.5 事务不生效的排查思路遇到下单后库存没扣或者只插入了一半数据的情况优先检查三点第一Transactional有没有加在public方法上并且方法所在类是不是被Spring扫描第二rollbackFor有没有写Exception.class第三这个方法是Controller直接调用还是从Service外部调用如果是同类内部调用事务不会生效。排查事务问题最好的验证方式是手动抛一个异常看数据库里是否还留有脏数据回滚成功就是配置正确没回滚就继续照上面三点排查。7. 项目做完之后的几点体会整个项目从建表到全部功能跑通我大概用了三周时间其中一半时间花在SSM配置和排错上。做完之后最大的感受是SSM这套组合值得每个做JavaWeb的人手动整合一遍因为你会真正理解Spring容器管理的对象从哪来、SpringMVC的请求怎么流转、MyBatis的SQL怎么和接口绑定。这些理解在直接使用Spring Boot时很难获得。如果后续要在这个项目上做扩展我建议从这几个方向入手用Redis做商品缓存和Session共享登录和权限控制升级成Spring Security或Shiro前端把JSP换成Vue做前后端分离MyBatis换成MyBatis-Plus简化SQL编写。每个方向都是简历上能写的实打实的技术点而且都是从这个项目的基础之上自然长出来的。最后再说一个经验配置文件和建表SQL千万不要直接复制网上的模板一定要自己逐行敲一遍。报错不可怕报错之后能看懂堆栈信息、能根据日志定位到具体配置问题这才是做这个项目最大的收获。把这套流程真正走通之后你对JavaWeb整个技术体系的理解会提升一个档次。