ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SSM框架家乡特产商城实战:从原理到部署的Java毕设完整解析

SSM框架家乡特产商城实战:从原理到部署的Java毕设完整解析

简介:面向Java毕业设计与课程设计人群,这是一份基于SSM框架的家乡特产网上商城完整项目包。项目采用Spring、SpringMVC、MyBatis加Vue构成B/S架构,配合MySQL数据库,覆盖商品展示、购物车、订单管理等典型电商模块,从前台浏览到后台管理均有对应实现,适合用于毕设选题、答辩演示或SSM+Vue方向的项目练手。压缩包共包含803个文件,以java源码、vue组件、js脚本、css样式及html页面为主,另有sql数据库脚本、xml配置、图片与gif动效资源,并附带说明文档和演示视频,整体约42.72MB。包内目录按前后端与部署脚本划分,包含建库脚本及启动/打包命令,可帮助快速搭建运行环境。目前已有125人学习下载,除源代码外还提供操作演示视频与部署说明,能帮助理解SSM与Vue的整合流程,节省从环境配置到功能联调的时间。

1. 这套 SSM 特产商城,为什么值得你亲手跑一遍

打开压缩包看到“源码+演示视频+说明”的时候,很多人的第一反应是找一份现成的毕业论文拼凑交差。但如果你真打算靠这个 Java 毕设项目过答辩、进实习,我劝你换个姿势:把它当成一条已经铺好的路,你负责把每个路口都走一遍。基于 SSM 框架的家乡特产网上商城,本质是 Spring MVC + Spring + MyBatis 三件套组合出来的典型电商系统,覆盖用户注册登录、商品浏览、购物车、下单支付模拟、后台订单管理和会员管理。它不像 Spring Boot 那样把配置藏起来,反而因为 SSM 的“手动拼装”特性,能让你在简历上实打实写出“熟悉 SSM 架构与原理”。

这套项目适合谁?适合那些框架原理说不利索、想在答辩前把知识点补齐的应届生,也适合想快速搭一个电商原型做课程设计的同学。它最大的价值不是“能跑”,而是你能在一周内把从前端页面到数据库表的整条链路讲清楚。我见过太多人拿着源码却跑不起来,最后卡在环境变量、依赖缺失、数据库版本这类基础问题上。这篇文章就按我自己的交付习惯,把这个商城的框架选型、核心实现、部署步骤和典型坑位全部拆开讲,保证你跟着走能复现,遇到报错能定位。

2. 为什么是 SSM 而不是 Spring Boot:从毕业设计答辩角度选型

2.1 选型理由:SSM 能让你把“原理”说清楚

这两年 Spring Boot 已经是企业级开发的主流,但很多高校的毕业设计题目里仍然写着 SSM。原因不在于学校落后,而在于 SSM 的配置是显式的:数据源、事务管理器、视图解析器、Mapper 扫描,每一个组件都要你亲手声明。这种“手动挡”的体验,恰恰是答辩时展示功力的地方。

比如别人问“Spring 容器是怎么管理 Service 的”,用 Spring Boot 的人只能说“注解一标就自动注入了”;而你用 SSM,能顺着 web.xml 里的 ContextLoaderListener 讲到 ApplicationContext 的初始化过程,再说到<context:component-scan>的扫描路径。这套链路讲下来,老师会觉得你是真写过的人。

我一般会建议:如果你的毕设题目是“基于 XX 框架的 XX 系统”,先别急着换 Spring Boot 重写。题目挂的是 SSM,你就用 SSM 做完,答辩口径和论文结构完全对齐。换框架意味着论文里的架构图、核心代码、测试截图全要重做,得不偿失。真想在简历上补 Spring Boot,等答辩结束再迁移不迟。

2.2 项目骨架:标准的 Maven 多模块结构

SSM 商城最常见的结构是单 Maven 工程,按包名分层。拿到源码后先别急着启动,把目录结构认清楚:

ssm-shop/ ├── pom.xml ├── src/main/java │ ├── com.shop.controller # Spring MVC 控制器 │ ├── com.shop.service # 业务接口与实现 │ ├── com.shop.dao # MyBatis Mapper 接口 │ ├── com.shop.entity # 实体类,对应数据库表 │ └── com.shop.util # 工具类,如验证码、分页 ├── src/main/resources │ ├── jdbc.properties # 数据库连接配置 │ ├── spring-mybatis.xml # Spring 与 MyBatis 整合 │ ├── spring-mvc.xml # Spring MVC 配置 │ └── mapper # MyBatis 映射文件 └── src/main/webapp ├── WEB-INF/web.xml # 前端控制器与监听器注册 ├── WEB-INF/views # JSP 视图 ├── static # CSS/JS/图片 └── upload # 商品图片上传目录

这个结构有两点要注意。第一,webapp目录下的WEB-INF是 Java Web 项目的根,web.xml里配置了 DispatcherServlet、Spring 监听器、字符编码过滤器三件套;第二,mapper目录和dao接口的包路径必须一致,MyBatis 扫描时靠的是“接口全限定名 + XML 命名空间”做映射。如果你发现某个 Mapper 方法报了Invalid bound statement,十有八九是 XML 的 namespace 写错了。

2.3 核心依赖:pom.xml 里不能少的配置

SSM 的 pom.xml 是所有环境问题的源头。依赖版本不对、scope 写错、jar 冲突,都会导致启动时抛 ClassNotFoundException。我挑几个最常见的依赖拆一下:

<properties> <spring.version>4.3.7.RELEASE</spring.version> <mybatis.version>3.4.2</mybatis.version> <mysql.version>5.1.39</mysql.version> </properties> <dependencies> <!-- Spring 核心四件套 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <!-- MyBatis 与 Spring 整合包,注意是 mybatis-spring --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.1</version> </dependency> <!-- MySQL 驱动,scope 默认是 compile,不要写成 provided --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <!-- 连接池,阿里 druid 比 dbcp 好用,监控界面还能当加分项 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.29</version> </dependency> <!-- JSTL,JSP 页面里 <c:forEach> 标签依赖它 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies>

这里最坑的是javax.servlet-api的 scope。如果你把它配成provided,在 IDEA 里跑 Tomcat 没问题,因为 Tomcat 自身带了 servlet API;但如果你用java -jar打成一个 fat jar 去跑,就会立刻报NoClassDefFoundError。我的习惯是:毕设项目就在 IDEA 里用 Tomcat 插件跑,不折腾打包,provided反而是最安全的写法。

注意:如果你换用了更高版本的 MySQL 驱动(比如 8.0.x),jdbc.properties 里的驱动类要改成com.mysql.cj.jdbc.Driver,同时连接串必须加serverTimezone=Asia/Shanghai,否则会报时区错误。

3. 从建表到登录:数据库设计与权限模型的落地

3.1 五张核心表:商城系统的最小闭环

特产商城这种级别,不需要动辄十几张表。按我在这类毕业设计里最常见的方案,五张表就能跑通完整业务:用户表、商品表、分类表、订单表、订单明细表。如果需要做 banner 轮播图或公告,再加一张内容表即可。

-- 用户表:区分管理员和普通会员 CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录名', `password` varchar(100) NOT NULL COMMENT 'MD5加密后的密码', `phone` varchar(20) DEFAULT NULL, `role` tinyint(4) NOT NULL DEFAULT '1' COMMENT '角色:0管理员 1普通用户', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `idx_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 商品表 CREATE TABLE `product` ( `id` int(11) NOT NULL AUTO_INCREMENT, `category_id` int(11) NOT NULL COMMENT '分类外键', `name` varchar(100) NOT NULL, `subtitle` varchar(200) DEFAULT NULL COMMENT '副标题,列表页展示', `main_image` varchar(200) DEFAULT NULL COMMENT '主图路径', `detail` text COMMENT '富文本详情', `price` decimal(10,2) NOT NULL, `stock` int(11) NOT NULL DEFAULT '0', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '1上架 0下架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

订单和订单明细为什么要拆成两张表?因为一个订单可能包含多件商品,只有拆开才能支持“订单总金额 = 明细金额之和”这个约束。之前看到有人把商品和数量直接拼成字符串存进订单表,答辩时被老师一眼看出不规范,这种设计在商城系统里是硬伤。

3.2 权限控制方案:拦截器 + Session 判断

商城天然分两端:前台用户用手机号或用户名注册登录,后台管理员管商品和订单。最简单的权限方案就是基于 Session 的角色判断,不需要引入 Shiro 或 Spring Security 这种重型框架——毕设里用框架反而容易把自己绕晕。

先在 Spring MVC 配置文件里注册一个登录拦截器:

<!-- spring-mvc.xml 中配置拦截器 --> <mvc:interceptors> <mvc:interceptor> <!-- 拦截所有请求,但排除登录页和静态资源 --> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login"/> <mvc:exclude-mapping path="/register"/> <mvc:exclude-mapping path="/static/**"/> <bean class="com.shop.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>

再写拦截器逻辑:

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 从 Session 中取用户 Object user = request.getSession().getAttribute("loginUser"); if (user == null) { // 未登录,重定向到登录页 response.sendRedirect(request.getContextPath() + "/login"); return false; } // 管理员路径,只允许 role=0 通过 String uri = request.getRequestURI(); if (uri.contains("/admin/")) { User loginUser = (User) user; if (loginUser.getRole() != 0) { response.sendRedirect(request.getContextPath() + "/index"); return false; } } return true; } }

这个方案的优点是直观、好讲。拦截器和 HandlerInterceptor 的关系,以及 preHandle 返回 false 后请求不再继续走,这两句话是答辩高频考点。缺点是如果商城规模变大,Session 不适合横向扩展,但毕设场景完全够用。

提示:密码千万不要明文存储。用MD5(username + password)加盐再存,注册时算一次、登录时算一次做比对。MD5 不算强加密,但毕设里比明文强一个档次,答辩时能说出加盐的意图就够了。

3.3 商品分类的两种做法:单表还是自关联

分类表有两种设计。一种是纯单表,category里只有 id、name、sort_order;另一种是自关联,parent_id字段支持二级分类。特产商城通常建议用自关联,因为特产天然有“地方”属性:一级分类可以是“东北特产”“江南特产”,二级是具体品类。

自关联查询有个经典问题:怎么一次性查出两级分类?常见做法是 XML 里写递归映射:

<resultMap id="CategoryWithChildren" type="com.shop.entity.Category"> <id property="id" column="id"/> <result property="name" column="name"/> <collection property="children" ofType="com.shop.entity.Category" column="id" select="com.shop.dao.CategoryMapper.selectChildrenByParentId"/> </resultMap> <select id="selectAllWithChildren" resultMap="CategoryWithChildren"> SELECT id, name FROM category WHERE parent_id = 0 </select> <select id="selectChildrenByParentId" resultType="com.shop.entity.Category"> SELECT id, name FROM category WHERE parent_id = #{id} </select>

用 collection 加嵌套查询,MyBatis 会自动把子分类填充进去。这里有一个性能坑:如果分类层级深、数据多,会产生 N+1 查询问题。解决办法是对<collection>开启fetchType="lazy"延迟加载,前端只展示第一级时就不会触发子查询。不过对于特产商城这种最多两层的数据量,直接查出来反而更省事,懒加载在答辩时作为扩展点提一句就行。

4. 商品展示与购物车核心流程:从 Controller 到 Mapper 全链路

4.1 首页商品列表:分页查与动态 SQL

先看商品列表的 Controller。这里我用了 PageHelper 做分页,它是毕设里性价比最高的插件,一个 PageHelper.startPage 就能省掉手写 LIMIT 的麻烦。

@Controller public class ProductController { @Autowired private ProductService productService; // 首页列表:按分类筛选 + 关键词搜索 + 分页 @RequestMapping("/product/list") public String list(Integer categoryId, String keyword, @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "8") Integer pageSize, Model model) { PageHelper.startPage(pageNum, pageSize); List<Product> products = productService.findByCondition(categoryId, keyword); // 用 PageInfo 包装,前端拿 total、pages、list 三个属性足够 PageInfo<Product> pageInfo = new PageInfo<>(products); model.addAttribute("pageInfo", pageInfo); return "product/list"; } }

对应 Service 实现里,调用 Mapper 时传一个实体或 Map 作为查询条件。这里最容易被忽视的是 PageHelper 的静态方法必须在查询语句之前调用,而且只能作用于下一条 SQL。如果你在 service 里先执行了别的查询,再执行目标查询,分页就会串到前面那条上——这种“分页失灵”问题我排查过不止一次。

Mapper 的 XML 里用<if>标签拼动态条件:

<select id="findByCondition" resultType="com.shop.entity.Product"> SELECT id, name, subtitle, main_image, price, stock FROM product WHERE status = 1 <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND name LIKE CONCAT('%', #{keyword}, '%') </if> ORDER BY create_time DESC </select>

动态 SQL 是 MyBatis 的招牌能力,答辩时说到<if>标签的 test 表达式要重点解释:它走的是 OGNL,不是 EL,所以判断空字符串要写成keyword != null and keyword != '',不能偷懒只写一个。

注意:LIKE CONCAT('%', #{keyword}, '%')这种写法比直接拼接'%${keyword}%'安全得多。后者有 SQL 注入风险,哪怕毕设是内部演示,我也建议你养成用#{}的习惯,答辩时能主动说出“我用的是预编译参数”,印象分完全不一样。

4.2 购物车:用 Session 存还是用数据库存

购物车设计有两种流派:存 Session 和存数据库。存 Session 的好处是用户不需要登录也能加购,实现简单,但缺点是一关浏览器就全没了;存数据库则必须登录后才能用,数据能持久化,但要多写购物车表和增删改查。商城的正常逻辑是后者,但我见过不少毕设为了省事直接塞 Session,答辩时被问“购物车数据怎么持久化”就卡壳。

我的建议是:既然题目是“网上商城”,购物车表一定要有。哪怕只是简单实现,有了cart表,你的“购买流程”才能串成闭环:

// 加购逻辑:先查购物车是否已有该商品 // 有则数量 +1,没有则新增一条记录 public void addToCart(Integer userId, Integer productId, Integer quantity) { Cart exist = cartMapper.findByUserAndProduct(userId, productId); if (exist != null) { exist.setQuantity(exist.getQuantity() + quantity); cartMapper.updateQuantity(exist); } else { Cart cart = new Cart(); cart.setUserId(userId); cart.setProductId(productId); cart.setQuantity(quantity); cart.setChecked(1); // 默认勾选 cartMapper.insert(cart); } }

购物车列表的前端展示,需要把 Cart 和 Product 关联起来。这种关联查询写在 XML 里,用JOIN或子查询都行;我一般直接让 Cart 实体里带一个Product product属性,查询时用resultMap做关联映射,避免在 Java 代码里 for 循环拼装。

4.3 下单与库存扣减:事务必须加在 Service 层

商城系统里最容易出问题的是下单流程。正常的链路是:前端传来购物车勾选的商品列表,Service 里依次做“校验库存 -> 扣减库存 -> 生成订单 -> 生成订单明细 -> 清空购物车”。这个流程里任何一步失败,前面已经扣掉的库存都必须回滚,所以事务必须加在 Service 方法上。

@Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Autowired private OrderItemMapper orderItemMapper; @Autowired private ProductMapper productMapper; @Autowired private CartMapper cartMapper; @Override @Transactional(rollbackFor = Exception.class) public Integer createOrder(Integer userId, List<Cart> checkedCarts) { // 1. 计算总金额,并检查每件商品库存是否足够 BigDecimal totalPrice = BigDecimal.ZERO; for (Cart cart : checkedCarts) { Product product = productMapper.selectById(cart.getProductId()); if (product.getStock() < cart.getQuantity()) { throw new RuntimeException("商品「" + product.getName() + "」库存不足"); } totalPrice = totalPrice.add(product.getPrice().multiply( BigDecimal.valueOf(cart.getQuantity()))); } // 2. 生成订单主表 Order order = new Order(); order.setUserId(userId); order.setTotalPrice(totalPrice); order.setStatus(0); // 0待付款,1已付款,2已发货,3已完成 orderMapper.insert(order); // 3. 生成订单明细,并扣减库存 for (Cart cart : checkedCarts) { OrderItem item = new OrderItem(); item.setOrderId(order.getId()); item.setProductId(cart.getProductId()); item.setQuantity(cart.getQuantity()); orderItemMapper.insert(item); productMapper.reduceStock(cart.getProductId(), cart.getQuantity()); } // 4. 清空已下单的购物车 cartMapper.deleteByUserAndChecked(userId); return order.getId(); } }

事务注解@Transactional有几个参数要理解清楚。rollbackFor = Exception.class表示任何异常都回滚;默认情况下只回滚 RuntimeException,如果你抛的是自定义检查异常(比如库存不足算业务异常),不写这个会导致“库存扣了但订单没生成”的脏数据。另外要注意:自调用不走代理,如果你在同一个类里调this.createOrder(),事务是不生效的,必须从外部注入后调用。

扣减库存的 SQL 也很关键。标准写法是“先检查再更新”,但更严谨的做法是把检查放进 UPDATE 的条件里:

<update id="reduceStock"> UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock &gt;= #{quantity} </update>

这种写法叫乐观锁思路,一条 SQL 同时完成检查和扣减,如果影响行数为 0 说明库存不够。把这个细节写进论文的“系统设计”部分,是整篇文章的亮点。

5. 部署与排查避坑:从压缩包到可演示的完整链路

5.1 环境匹配:JDK、MySQL 版本与编码问题

拿到源码后的第一件事不是导入 IDEA,而是确认环境。SSM 这种老项目对环境特别敏感:JDK 版本太高可能不支持旧版 Spring,MySQL 8.0 和 5.7 的驱动类不一样,Tomcat 版本不同 web.xml 的写法兼容性也不同。我最推荐的组合是:JDK 1.8 + MySQL 5.7 + Tomcat 8.5。这三个版本都是 SSM 项目最活跃时期的标配,兼容性问题最少。

导入项目时,IDEA 里要注意两个设置。Project Structure 里的 SDK 选 1.8,Language Level 选 8;Maven 的 Runner 里 VM Options 加上-Dfile.encoding=UTF-8。不然控制台全是乱码——这是最常见的启动失败前兆。如果你发现数据库里查出来的中文是问号,而代码和页面都是 UTF-8,去检查 MySQL 的连接串有没有加characterEncoding=utf8,以及建表语句里有没有指定CHARSET=utf8mb4。

5.2 五条实战踩坑记录:现象、原因与解决

坑 1:Tomcat 启动直接报 ClassNotFoundException

现象:点击运行,IDEA 控制台刷了一堆错误,指向org.springframework.web.context.ContextLoaderListener。

原因:Maven 依赖没有正确加载,要么是pom.xml没有自动刷新,要么是本地仓库里缺 jar 包。更隐蔽的原因是 web.xml 里配的 Spring 配置文件路径不对,监听器初始化时找不到classpath:spring-mybatis.xml。

解决:先mvn clean再mvn compile看依赖能否正常解析;如果本地仓库缺包,检查网络后重新导入依赖。web.xml 里确认<context-param>的contextConfigLocation写的是classpath:spring-mybatis.xml,并且该文件确实存在于resources目录。

坑 2:MyBatis 报 Invalid bound statement (not found)

现象:启动不报错,但一调用某个 Mapper 方法就抛出Invalid bound statement (not found)。

原因:Mapper 接口的方法没有和 XML 里的<select>对应上。常见有三种:XML 文件里 namespace 写错了;接口方法名和 XML 的 id 不一致;XML 文件没有被 Maven 打包进target/classes(在src/main/java下建 mapper/xml 的人尤其容易踩这个)。

解决:看 XML 的 namespace 必须是接口的全限定名;接口方法名和 XML id 保持一字不差;如果 XML 放在src/main/java下,需要在 pom.xml 里加<resources>配置,把 xml 文件也纳入编译输出。

坑 3:数据库连接失败 Communications link failure

现象:启动时 Spring 创建数据源就报Communications link failure,或者页面里所有查库操作都 500。

原因:MySQL 服务没启动、端口不是 3306、用户名密码不对,或者 MySQL 8.0 和驱动 5.1 不匹配。毕设现场最常见的其实是 root 密码和自己设置的不一致。

解决:先用 Navicat 或命令行确认能connect到目标库;再检查jdbc.properties里 url、username、password 是否和本地一致。注意serverTimezone参数,MySQL 8.0 缺了它必报时区错。

坑 4:JSP 页面输出原样标签,没被渲染

现象:浏览器里看到的是<c:forEach>这些标签原文,而不是渲染后的列表。

原因:缺少 JSTL 依赖,或者 web.xml 的 servlet 版本声明太低导致 JSP 引擎不走 JSTL 支持。另外如果你用 HTML 文件名当成 JSP 访问,也可能出现这种问题。

解决:pom.xml 加javax.servlet:jstl:1.2,并且确认 JSP 页面开头写了<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>。改完重启前执行一次mvn clean,避免旧 class 文件干扰。

坑 5:部署后上传的图片无法显示

现象:后台传商品图成功,但前台<img>标签 404。

原因:IDEA 内置 Tomcat 的工作目录和项目target目录不一致,上传文件写入了target/upload,但下次重启或清理 target 后图片就没了。

解决:在磁盘上单独建一个D:/upload目录作为图片存储路径,并在 spring-mvc.xml 里配置静态资源映射:<mvc:resources mapping="/upload/**" location="file:D:/upload/"/>。这样图片不随项目清理丢失,也是企业里常用的做法。

6. 演示视频之外:把“会做”变成“讲得清楚”的验证方法

拿到演示视频别光看。我通常建议的做法是:对照视频里的每个操作,在本地手动复现一遍,然后关掉视频,自己从头到尾点一遍。这个流程能暴露大量“眼睛会了手不会”的问题,比如你根本不知道商品分类是在哪个按钮下添加的,或者后台账号要从数据库里直接改 role 字段。

更进阶的验证方式,是把视频里的功能清单列成表格,每完成一项就打勾。最基础的清单包括:用户注册登录、商品分页浏览、关键词搜索、加入购物车、购物车数量修改、提交订单、后台商品增删改、图片上传、订单发货。这十个功能覆盖了“用户端 + 管理端”的最小闭环。你不但要能点通,还要能说出每一步对应哪个 Controller 方法、哪个 JSP 页面——答辩时老师最爱的问法是从页面上反问你“这个按钮提交到哪个地址”,答不上来就露怯。

还要做的一件事,是人为制造一个错误。比如把某件商品库存改成 0,然后走下单流程,观察页面会不会提示“库存不足”。这种异常分支的验证,比单纯跑通正常流程更能体现你对业务的理解。论文的“系统测试”章节也需要这部分内容,写“功能测试通过”是凑字数,写“库存不足场景下订单回滚正常”才是干货。

说到回滚,我再补一个自己曾经翻车的教训:演示时明明抛了异常,库存却已经扣掉了。排查半天发现是 Service 方法内的自调用——事务没生效。后来的习惯是,凡是写写操作,一律在方法入口打一条日志,先确认是不是走的事务代理。这类边界问题,你在毕业论文的“关键技术”部分如果能用一页篇幅讲清楚,比罗列十张截图都管用。希望这篇能帮你在答辩前少踩几个坑,把压缩包里的源码真正变成自己讲得清、改得动的东西。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进