资讯动态

SSM框架电脑配件商城实战:B/S架构与订单库存并发处理

发布时间:2026/10/26 23:23:59 来源:尧图企业网站定制
简介这是一份基于SSM框架实现的电脑配件销售系统完整毕业设计项目面向计算机相关专业学生或有商城类系统开发需求的读者覆盖管理员与普通用户双端核心流程既适合课程设计也适合毕业设计答辩展示。后端采用Spring、SpringMVC、MyBatis前端以JSP、CSS、JavaScript实现配合Maven构建与MySQL数据库源码可在Eclipse或IDEA中直接运行压缩包共1344个文件、约22.21MB包含Java源码、JSP页面、SQL脚本及大量JS、CSS、图片等静态资源目录结构清晰便于定位功能模块。系统后台具备登录、用户管理、商品分类与信息管理、上下架、库存调整、订单管理、轮播图管理等功能前台支持注册登录、商品推荐、收藏、购物车、立即购买、评论、余额与积分充值、支付等真实商城业务场景。已有133人学习下载配套完整源码和数据库脚本适合需要快速搭建可运行毕设项目或研究SSM整合流程的读者。1. 为什么一个SSM的电脑配件商城到现在还有人在做毕设和练手每年求职季的技术社群里总能看到同一类问题简历上写着“基于SSM框架的电脑配件销售商城”面试官追问几句就卡壳。SSM 这个词从来不算冷门Spring、SpringMVC、MyBatis 三者怎么分工B/S 架构下数据从浏览器到 MySQL 再回来要过几道门很多人其实没有完整跑通过一次源码。这个选题在毕业设计里确实已经“烂大街”但越是烂大街的组合越能检验基本功——它把 Maven 依赖管理、三层架构、ORM 映射、声明式事务、Session 购物车、订单减库存这些 Java Web 绕不开的硬知识点串在了一起。如果你正在准备毕业设计答辩或者想找一份带数据库的完整源码做练手项目这篇就按从架构讲起一路落到建表 SQL、运行步骤和最容易翻车的五个坑。2. B/S架构下的SSM拆分Spring、SpringMVC、MyBatis各自守着哪一层2.1 三层架构与SSM的对应关系谁在管请求、谁在管事务、谁在管SQLB/S 架构里浏览器是唯一的客户端用户访问商城只需要输入网址不需要安装任何桌面程序。请求从浏览器发出后第一个接收者是 TomcatTomcat 把请求交给 SpringMVC 的前端控制器 DispatcherServlet。DispatcherServlet 做的事情是“路由”通过 HandlerMapping 找到处理这个 URL 的 Controller 方法并调用再把返回值交给视图解析器渲染成 JSP 页面返回给浏览器。整个过程对用户来说就是打开页面、点按钮数据交换都基于 HTTP。Controller 这一层是表现层它只负责接收参数、调用业务方法、封装返回结果不直接操作数据库。真正管对象的是 Spring 容器Service 层的实现类被 Spring 以单例 Bean 的方式管理依赖注入让 Controller 不用自己 new Service事务边界也由 Spring 的 Transactional 注解控制。最底层是 MyBatis它把 Java 接口方法映射成 SQL 语句负责和 MySQL 打交道。这三者的职责边界可以简单记成SpringMVC 管“请求怎么进来怎么出去”Spring 管“业务对象生命周期和事务”MyBatis 管“SQL 怎么写怎么执行”。一个典型的商品查询在 SSM 里长这样Controller、Service、Mapper 各占一层Controller RequestMapping(/parts) public class PartsController { Autowired private PartsService partsService; RequestMapping(/list) public String list(Integer categoryId, Model model) { ListParts list partsService.findByCategory(categoryId); model.addAttribute(partsList, list); return parts/list; } }Service Transactional public class PartsServiceImpl implements PartsService { Autowired private PartsMapper partsMapper; public ListParts findByCategory(Integer categoryId) { return partsMapper.selectByCategory(categoryId); } }public interface PartsMapper { ListParts selectByCategory(Integer categoryId); }逻辑说明URL/parts/list带着参数categoryId进来DispatcherServlet 根据RequestMapping找到list方法Autowired注入的partsService是 Spring 在启动时就创建好的单例方法内部一行代码把查询交给了 Mapper 接口。Transactional加在 Service 实现类上表示这个类里的方法都处于事务管理范围不过只读的查询操作本身不会产生多少事务开销真正需要事务的是后面讲的订单提交逻辑。参数说明Controller注解告诉 Spring 这是一个接收 Web 请求的 Bean类上的RequestMapping(/parts)是 URL 前缀方法上的RequestMapping(/list)拼接成完整路径/parts/listModel对象等同于 request 作用域放入的partsList在 JSP 里可以直接用${partsList}取。Service 接口和实现类分开写是为了后面做单元测试和 AOP 切面时能替换实现新手图省事把 Service 直接写成类也能跑但面试官通常不接受这种写法。SSM 的配置会拆成三份文件spring-mvc.xml 只扫描 Controller 层applicationContext.xml 扫描 Service、Mapper 等业务 Beanmybatis-config.xml 配置 MyBatis 本体的行为。这样拆分的原因是 DispatcherServlet 只会为 Controller 层提供 Web 特性比如视图解析、JSON 转换如果让它连带扫描 Service 包会导致 Bean 重复实例化最直接的症状就是 Transactional 注解失效。新手最常见的错误就是把所有包都交给 spring-mvc.xml 扫描结果事务怎么配都不生效这个问题在第 5 章还要细说。2.2 电脑配件商城的核心业务流从商品列表到订单减库存商城真正的核心不是展示商品而是下单那一刻的“库存扣减订单生成”。用户浏览配件、加入购物车、结算提交订单整个过程可以拆成四个阶段选品、存购物车、提交订单、支付毕设里的支付通常用模拟按钮替代。在 SSM 实现里购物车有两种存法存在 Session 里或者存在数据库购物车表中。毕设项目普遍选 Session因为不用每次操作都读写数据库用户未登录也能临时加购生产环境选数据库或 Redis 的更多因为 Session 在集群环境下没法共享。提交订单这段代码是整个项目里事务价值最大的地方也是面试官最爱追着问的一段。我一般会用一个独立的 createOrder 方法把主表插入、明细插入、库存扣减串起来Service public class OrderServiceImpl implements OrderService { Autowired private OrderMapper orderMapper; Autowired private OrderItemMapper orderItemMapper; Autowired private PartsMapper partsMapper; Transactional(rollbackFor Exception.class) public Long createOrder(Integer userId, ListCartItem cartItems) { Order order new Order(); order.setUserId(userId); order.setOrderNo(generateOrderNo()); order.setStatus(0); orderMapper.insert(order); Long orderId order.getId(); int totalAmount 0; for (CartItem item : cartItems) { Parts parts partsMapper.selectById(item.getPartsId()); if (parts null || parts.getStock() item.getNum()) { throw new RuntimeException(配件库存不足 item.getPartsId()); } int rows partsMapper.reduceStock(item.getPartsId(), item.getNum()); if (rows 0) { throw new RuntimeException(配件库存扣减失败 item.getPartsId()); } OrderItem oi new OrderItem(); oi.setOrderId(orderId); oi.setPartsId(parts.getId()); oi.setPrice(parts.getPrice()); oi.setNum(item.getNum()); orderItemMapper.insert(oi); totalAmount parts.getPrice() * item.getNum(); } order.setTotalAmount(totalAmount); orderMapper.update(order); return orderId; } private String generateOrderNo() { return System.currentTimeMillis() (int) (Math.random() * 900 100); } }这段代码的核心在于Transactional(rollbackFor Exception.class)。默认情况下 Spring 事务只对 RuntimeException 回滚加上rollbackFor Exception.class表示任何异常都触发回滚。有了这一句循环里任何一步抛异常之前插入的订单主表、明细表和扣减掉的库存全部回滚不会出现“库存扣了订单没生成”的脏数据。代码里还有一个关键细节reduceStock在 Mapper XML 里的 SQL 用了“带条件更新”也就是UPDATE parts SET stock stock - #{num} WHERE id #{id} AND stock #{num}它不是先查库存再更新而是让数据库在原子操作里判断库存是否足够这样并发请求不会重复下单。数据量小的课设里这条 SQL 就够了生产环境还要引入 Redis 预扣库存等措施。代码里另一个容易被问到的点是事务边界。整个 createOrder 方法是一个事务扣库存和插入订单在同一个事务里要么全成要么全败。但要注意事务和锁不是一回事——行锁在 UPDATE 执行时获得在事务提交或回滚时释放如果事务不提交锁就不会释放。所以事务方法里千万别做耗时操作比如调模拟支付接口的 Thread.sleep否则并发下其他用户扣库存会一直阻塞等锁压测时表现就是接口卡死、连接池耗尽。这个我在第 5 章会作为踩坑案例再说一次。3. 把数据库表设计出来配件分类、库存与订单的ER关系落地3.1 六张核心表的字段设计与类型选择电脑配件销售商城按最常见的需求拆成六张核心表用户表、配件分类表、配件表、购物车表、订单主表、订单明细表。先看建表 SQL再看字段为什么这么定。为了少敲命令这里直接把外键、唯一键和索引都一并建好CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(64) NOT NULL COMMENT 建议存MD5或加盐哈希, phone VARCHAR(20) DEFAULT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE category ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE parts ( id INT NOT NULL AUTO_INCREMENT, category_id INT NOT NULL, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, description TEXT, image_url VARCHAR(200) DEFAULT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id), CONSTRAINT fk_parts_category FOREIGN KEY (category_id) REFERENCES category (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE cart ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, parts_id INT NOT NULL, num INT NOT NULL DEFAULT 1, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_parts (user_id, parts_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已发货 3已完成 4已取消, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_item ( id INT NOT NULL AUTO_INCREMENT, order_id INT NOT NULL, parts_id INT NOT NULL, price DECIMAL(10,2) NOT NULL COMMENT 下单时的成交价快照, num INT NOT NULL, PRIMARY KEY (id), KEY idx_order (order_id), CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES orders (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段类型这部分有四个细节容易被忽略。第一价格必须用 DECIMAL(10,2)不能用 float 或 double因为二进制浮点数存 10.23 这种值会出精度误差结算金额对不上账用 DECIMAL 虽然占用空间稍大但商城项目这点成本可以忽略。第二密码字段长度给 64 而不是 32是为了兼容加盐之后的 SHA-256 哈希值很多毕设代码里明文存密码答辩时被问到就直接露怯。第三order_item 里存了 price 快照——配件改价之后历史订单不能跟着变这是关系型数据库设计里“冗余字段”的典型场景面试官问为什么要冗余时能答上来就是加分项。第四orders 表的 order_no 建了唯一索引业务上靠它兜底保证订单号不重复光靠代码里的随机数不保险。3.2 订单号生成与库存扣减用事务还是用锁订单号生成在第 2 章给的临时方案是“时间戳随机数”放在课设里够用但有两个隐患一是同一毫秒并发下单可能撞号二是数据库一旦有重复值插入直接报错。更可靠的替代是把数量来源交给数据库比如利用自增 id 拼接时间前缀或者用 sequence。生产级方向是雪花算法SSM 课设阶段不强制但你要在论文里写清楚“为什么选时间戳方案改进方向是什么”。库存扣减是并发问题的重灾区。网上很多教程写“先 SELECT 查库存再 UPDATE 减库存”这是错的。假设库存只剩 1 件两个用户同时下单都查到 1都执行减库存超卖就发生了。正确做法是让扣减语句带上库存条件也就是第 2 章 createOrder 里的 reduceStockUPDATE parts SET stock stock - #{num} WHERE id #{id} AND stock #{num}这条 UPDATE 在 MySQL 的 InnoDB 引擎下会对命中行加行锁两个并发请求同时执行时后执行的那个会因为 stock #{num} 条件不满足而影响 0 行。MyBatis 里返回的 int rows 就是受影响行数Service 层判断 rows 0 就知道库存不足并抛异常回滚。这个方案不需要显式写 SELECT ... FOR UPDATE 的悲观锁性能比锁表好是电商库存扣减里最常用的一种条件更新乐观锁写法。这里还要澄清一个概念事务和锁不是一回事。事务保证的是“要么全做要么全不做”锁保证的是“同一时刻只有一个请求能改这行数据”。上面的 UPDATE 语句本身带了行锁所以即使在 autocommit 模式下执行两个请求也不会同时把库存减成负数但如果你要把“扣库存写订单”放在一个事务里就必须在 Service 方法上加 Transactional让 UPDATE 和 INSERT 在同一个数据库会话里提交否则库存减了但订单没写成数据就错了。4. 跑通一个最小SSM商城的步骤从Maven骨架到第一次CRUD4.1 项目结构搭建与pom.xml关键依赖先建一个标准的 Maven Web 项目目录结构不能错。SSM 项目新手最容易在结构上翻车约定的布局如下src/main/java/com/example/mall ├── controller # SpringMVC控制器 ├── service # 业务接口 │ └── impl # 业务实现类 ├── mapper # MyBatis Mapper接口 └── pojo # 实体类 src/main/resources ├── spring-mvc.xml ├── applicationContext.xml ├── mybatis-config.xml └── mapper/*.xml # SQL映射文件 src/main/webapp ├── WEB-INF/web.xml └── WEB-INF/views/*.jsppom.xml 的依赖按作用域分四组缺一不可。Spring 5.2 配 JDK 8MyBatis 3.5 是当前主流稳定线MySQL 驱动版本要和本地数据库对号入座properties spring.version5.2.x.RELEASE/spring.version mybatis.version3.5.x/mybatis.version /properties dependencies !-- Spring核心webmvc里包含了spring-context和spring-web -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency !-- Transactional依赖spring-jdbc提供事务管理器 -- dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency !-- MyBatis与Spring集成 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.x/version /dependency !-- MySQL驱动与连接池 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.x/version scoperuntime/scope /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.x/version /dependency !-- JSP / JSTL、Servlet依赖 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency /dependencies版本段位别乱配Spring 5.2 对应 JDK 8Spring 6 强制要求 JDK 17如果电脑装的是 JDK 8 而依赖却拉到了 Spring 6启动时会直接报 UnsupportedClassVersionError。mybatis-spring 2.0.x 配 MyBatis 3.5.x 是经过大量项目验证的组合不建议为了求新随意换大版本。依赖里容易踩的坑是漏掉 spring-jdbc。不少教程只引 spring-webmvc结果 Transactional 对应的 DataSourceTransactionManager 找不到类启动就报 ClassNotFoundException。mybatis-spring 这个桥接包负责把 MyBatis 的 SqlSessionFactory 交给 Spring 容器管理没有它 Mapper 接口无法被自动扫描注入你会看到所有 Mapper 注入的地方都报空指针。4.2 用MyBatis写第一个配件商品查询Mapper接口与XML映射数据库表建好后第一步先跑通“按分类查配件列表”这个闭环。实体类对应 parts 表字段一一映射。注意用 BigDecimal 对应 DECIMAL 字段用 Integer 对应 INT 字段别混用public class Parts { private Integer id; private Integer categoryId; private String name; private BigDecimal price; private Integer stock; private String description; private String imageUrl; private Integer status; private Date createTime; // getter/setter 省略 }Mapper 接口和 XML 映射文件要保证 namespace 完全对应。接口放在 com.example.mall.mapper 下public interface PartsMapper { ListParts selectByCategory(Param(categoryId) Integer categoryId); int reduceStock(Param(id) Integer id, Param(num) Integer num); }XML 放到 src/main/resources/mapper/PartsMapper.xmlapplicationContext.xml 里用 mapperLocations 指向这个目录?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.mall.mapper.PartsMapper resultMap idBaseResultMap typecom.example.mall.pojo.Parts id columnid propertyid jdbcTypeINTEGER/ result columncategory_id propertycategoryId jdbcTypeINTEGER/ result columnname propertyname jdbcTypeVARCHAR/ result columnprice propertyprice jdbcTypeDECIMAL/ result columnstock propertystock jdbcTypeINTEGER/ result columndescription propertydescription jdbcTypeVARCHAR/ result columnimage_url propertyimageUrl jdbcTypeVARCHAR/ result columnstatus propertystatus jdbcTypeTINYINT/ result columncreate_time propertycreateTime jdbcTypeTIMESTAMP/ /resultMap select idselectByCategory resultMapBaseResultMap SELECT id, category_id, name, price, stock, image_url FROM parts WHERE category_id #{categoryId} AND status 1 ORDER BY id DESC /select update idreduceStock UPDATE parts SET stock stock - #{num} WHERE id #{id} ![CDATA[ AND stock #{num} ]] /update /mapper逻辑说明selectByCategory 里的 #{categoryId} 是预编译占位符MyBatis 会转成 ? 再绑定参数能防 SQL 注入千万别在 XML 里写 ${categoryId} 这种字符串拼接。resultMap 的作用是把下划线列名和驼峰属性名做显式映射Java 里属性叫 categoryId表里列叫 category_id不写映射就查出来全是 null——这是 MyBatis 新手最常见的“对象怎么是空”翻车原因。reduceStock 里的 CDATA 块是为了让 XML 解析器不把 当成标签符号这也是第 3 章说的并发扣库存的落地写法。参数说明Param(categoryId) 让 XML 里能用 #{categoryId} 引用参数如果不加注解多参数的 XML 里只能用 #{param1}、#{param2} 这种位置引用可读性差还容易写错。mybatis-config.xml 里还可以打开驼峰映射开关 mapUnderscoreToCamelCasetrue这样大部分简单字段就不用写 resultMap 了但聚合查询和特型字段还是要手写映射。4.3 前端页面与后端联调B/S架构下session和购物车怎么存后端查询通了接下来把 JSP 页面挂上去。SSM 课设的页面一般是 JSP JSTL 服务端渲染浏览器发请求带上 JSESSIONID Cookie 维持会话。购物车存在 Session 里是最省事的方案实现就两步RequestMapping(/cart/add) public String addToCart(Integer partsId, Integer num, HttpSession session) { MapInteger, Integer cart (MapInteger, Integer) session.getAttribute(cart); if (cart null) { cart new HashMap(); session.setAttribute(cart, cart); } cart.put(partsId, cart.getOrDefault(partsId, 0) num); return redirect:/parts/list; }c:forEach items${cart} varentry tr td${entry.key}/td td${entry.value}/td /tr /c:forEach这段 Controller 的逻辑是Session 里放一个 MappartsId, num用户每次点“加入购物车”就往 Map 里累加数量购物车页面直接遍历 Map 渲染。优点是省 SQL、不建表浏览器关闭后 Session 销毁购物车自动清空演示完即走缺点是换浏览器或清 Cookie 就丢数据生产环境必须把购物车挪到 Redis 或数据库。联调时有个常见问题页面上想显示配件名称而不是数字 ID有些毕设代码会在 JSP 里直接调 Service比如${partsService.getById(entry.key).name}。这种写法的危害是 JSP 本质是视图直接依赖业务层会绕开 Controller 的参数校验和权限控制而且 Service 未必在页面上下文里可用。正确做法是让 Controller 根据 Map 查出配件列表放进 ModelJSP 只负责展示。遇到页面上取不到值先看 Model 里有没有数据再看 EL 表达式拼写是否和属性名一致九成空白页都是这两个原因。提示把 mybatis 和 spring 的日志级别调到 DEBUG第一次联调就能在控制台看到执行的 SQL、参数和返回行数比自己猜“为什么没数据”高效得多。5. SSM商城避坑清单五个让新手翻车的经典问题SSM 商城跑通不难但把项目从“能跑”变成“经得起问”要过不少坎。以下五条按出现频率排序每一条都是运行日志里最常见的报错场景。排查前先做一件事把 org.mybatis、org.springframework.jdbc、com.alibaba.druid 三个包的日志级别调到 DEBUG让 SQL 和连接池的每一步都打印出来很多问题一眼就能定位。5.1 现象Controller跳转404但浏览器直接访问接口有数据原因SpringMVC 的 web.xml 里 DispatcherServlet 的 url-pattern 配成了/静态资源、JSP 页面都被拦截导致视图跳转后找不到页面。更隐蔽的是 InternalResourceViewResolver 的前缀后缀配错JSP 明明在 /WEB-INF/views/ 下前缀却写成 /WEB-INF/解析出来的路径对不上。解决确认 url-pattern 是/还是*.do配成/时必须在 spring-mvc.xml 里加mvc:default-servlet-handler/或mvc:resources放行静态资源再看视图解析器配置bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /bean一个排查技巧浏览器直接访问/parts/list能出数据说明 Controller 没错跳转 404 几乎都是视图解析路径的问题。把前缀改成 /WEB-INF/views/ 后重启九成能好。5.2 现象表单提交的中文到Controller变成问号写进数据库再变成乱码原因CharacterEncodingFilter 过滤器没配置或配了但 forceEncoding 没设 true。Tomcat 8 对 GET 请求的 URI 默认用 UTF-8 解码但 POST 请求体的编码取决于请求头 Content-Type 和页面声明的字符集前端页面是 UTF-8、后端按 ISO-8859-1 解码就会乱码。解决在 web.xml 里加字符编码过滤器放在所有过滤器最前面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同时检查数据库连接 URL 里带了 characterEncodingutf8三处统一成 UTF-8 后乱码才不会反复。注意 JSP 页面头部的 pageEncoding 也要写 UTF-8漏一个环节就会出现“数据库里是好的页面显示乱码”的怪象。5.3 现象项目启动后第一次访问正常隔几分钟再访问报 Communications link failure原因MySQL 默认 wait_timeout 是 8 小时但开发机上的 MySQL 或云数据库会把空闲太久的连接断掉Druid 连接池不知道继续把旧连接发给业务拿到连接执行 SQL 时就抛异常。表现是项目跑一会儿再访问就报错重启 Tomcat 又好转典型的连接池“幽灵连接”。解决在 Druid 连接池配置里打开探活property nametestWhileIdle valuetrue/ property namevalidationQuery valueSELECT 1/ property nametimeBetweenEvictionRunsMillis value60000/testWhileIdle 让连接池定期用 SELECT 1 验证空闲连接无效就直接丢弃并新建timeBetweenEvictionRunsMillis 控制在 60 秒一次资源开销很小。如果项目里还有定时任务给 Druid 的 minEvictableIdleTimeMillis 也要设一个小于 MySQL wait_timeout 的值。5.4 现象MyBatis 的 XML 里写 stock #{num} 直接报错提示语法错误原因XML 中是保留字符解析器把当成标签开始符号SQL 里一旦出现小于号、小于等于号必炸。没人帮你转义IDEA 里的 XML 校验会直接划红线。解决把转义为lt;或者用 CDATA 包住整个条件。我在 4.2 节的 reduceStock 里已经写了标准写法这里单独强调一次update idreduceStock UPDATE parts SET stock stock - #{num} WHERE id #{id} ![CDATA[ AND stock #{num} ]] /update这条 SQL 就是并发扣库存的正式落地reduceStock 返回的 int 在 Service 里判断等于 0 就是库存不足。CDATA 里的内容 XML 解析器不检查特殊字符但要注意 CDATA 里不能再用![CDATA[嵌套。5.5 现象购物车在本地测试没问题一部署到服务器上刷新就丢原因Session 的存取依赖 Cookie。部署后访问 IP 或域名变了浏览器本地的 JSESSIONID Cookie 没跟着更新或者项目没配置 Session 超时时间默认 30 分钟没操作就过期。另一个隐蔽原因是多节点部署时负载均衡把请求分到不同 TomcatSession 不共享。解决单机部署先在 web.xml 里调 Session 超时session-config session-timeout120/session-timeout /session-config如果项目已经上了多节点就不要想着靠 Session 了方向是“Session 外置”——把购物车数据从 Session 挪到 Redis用同一个 Redis 实例做共享存储。第 6 章会简单说这个迁移怎么做。毕设如果演示环境是单机 TomcatSession 方案完全够用但要在论文里写明局限。这五条是从本地复现 SSM 商城时踩过的坑按优先级排出来的从页面打不开到并发扣库存每一条都对应真实的运行日志报错。把这些现象、原因、解决办法记熟比多写一百行 CRUD 代码更能撑住答辩场面。6. 拿这套商城做验收三个验证手段和一个可落地的扩展方向6.1 三个验证手段从SQL日志到并发压测项目跑通后别急着写论文先用三个手段验证它真的“站得住”。第一个是 SQL 日志验证把日志级别调到 DEBUG 后连续操作一个完整流程——浏览配件、加购物车、提交订单、取消订单观察每次操作对应的 SQL 是否按预期执行尤其盯住 createOrder 方法里的 INSERT、UPDATE 是不是在同一个事务里提交日志里能看到明显的 BEGIN 和 COMMIT。第二个是接口验证用 Postman 把/parts/list、/cart/add、/order/create三个接口按顺序走一遍检查返回的状态码和重定向地址。B/S 架构下很多接口返回的是 JSP 视图名走 Postman 看到的是 HTML 片段不要慌重点看 HTTP 状态是 200 还是 500。如果有 500回服务器看日志堆栈把第 5 章的对应条目翻出来对号入座。第三个是简单的并发压测本地用 JMeter 开 50 个线程同时请求/order/create模拟两个用户抢最后一件库存的场景。跑完去数据库查 parts 表的 stock 和 orders 表记录数如果 stock 不为负数且订单数和实际扣减库存对得上说明 reduceStock 的条件更新生效了。这一步是答辩时最亮的加分点——几乎没人会做并发验证。6.2 一个值得投入的扩展方向把Session购物车换成Redis如果还想往上走一步最有性价比的扩展是把 Session 购物车迁移到 Redis。思路很明确Controller 里不再从 HttpSession 取 Map而是从一个 CartService 读取CartService 内部用 Redis 的 Hash 结构存储 userId → partsId → num。Service public class CartService { Autowired private StringRedisTemplate redisTemplate; public void add(Integer userId, Integer partsId, Integer num) { String key cart: userId; redisTemplate.opsForHash().increment(key, partsId.toString(), num); } public MapObject, Object getCart(Integer userId) { String key cart: userId; return redisTemplate.opsForHash().entries(key); } }这段代码的逻辑是把购物车从“会话内存”搬到“独立缓存”对业务代码的改动只局限在 Controller 的取值方式上订单生成的逻辑完全不用动。Redis 的 Hash 天然适合购物车这种“一个用户多个商品”的结构increment 还能原子累加数量。迁移完成后Session 里只剩登录状态购物车数据不会再因为重启 Tomcat 而丢失。如果想把工程化程度再提高把 Mapper 层换成 MyBatis-Plus用实体类注解直接生成建表 SQL 和通用增删改查也是课设升级里很常见的一步。这个方向值得投入的原因很实际它覆盖了缓存设计、Key 设计、序列化三个面试高频考点而且改动量控制在一周以内。做完这步再把订单号改成雪花算法把支付状态改成调用第三方沙箱这套课设就能从“毕设源码”升级成“简历里敢写、面试敢聊”的项目。我自己第一次做 SSM 商城时跑通是跑通了但被面试官一句“库存怎么防止超卖”问得哑口无言。后来把扣库存改成条件更新把事务边界理清楚才敢说这个系统是自己的。希望你绕开这些坑把电脑配件商城做得比普通课设多一点思考。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑