SSM零食商城实战:高并发库存控制与Tomcat部署
2026/9/11 23:25:37 网站建设 项目流程

简介:这是一套面向计算机专业本科生的JavaWeb毕业设计实战资源,聚焦SSM框架下的零食商城系统开发,完整覆盖从需求分析、数据库设计到前后端模块实现的全流程。资源包含详细论文文档(含E-R图、系统测试与使用说明)及可运行源码,适合课程设计、毕设参考与SSM技术栈进阶学习。压缩包共1690个文件,涵盖182个核心Java类、532个编译后Class文件、138个依赖Jar包、114个配置XML、88个JSP页面及配套JS/CSS/图片资源,整体128.98MB,结构清晰便于按模块研读。已有6163人学习下载,读者可直接部署运行,深入理解用户购物、后台管理等关键模块的设计逻辑,并获取SSM整合实践、MySQL建表规范、前端页面交互实现等一线开发经验。

1. 这不是又一个“SSM CRUD 演示”,而是一个能跑在真实 Tomcat 上、带完整支付闭环和库存强校验的零食商城系统

你搜“javaweb 零食商城 ssm”时,大概率会看到一堆只有首页+登录+商品列表的半成品项目——它们连购物车清空后重新加购是否重复生成新条目都没验证,更别说下单时并发扣减库存导致超卖。但真实企业级 JavaWeb 小型电商系统(比如校园周边无人售货机后台、社区团购前置仓管理端)必须处理:用户登录态与权限隔离(普通买家 vs 店铺管理员)、购物车跨设备同步、订单状态机驱动(待支付→已支付→发货中→已完成)、MySQL 中 InnoDB 行锁与乐观锁混合控制库存、以及最关键的——所有业务操作必须可审计、可回溯、可对接后续的物流/财务模块。本项目正是按这个标准构建的 SSM(Spring + SpringMVC + MyBatis)落地案例:它不依赖任何云服务或第三方 SDK,全部逻辑封装在service层,数据库脚本含完整外键约束与索引,源码中每个@Transactional注解都标注了传播行为与回滚条件,论文部分则聚焦于“如何用 MyBatis 的<foreach>SELECT FOR UPDATE组合解决高并发下单场景下的库存一致性问题”。适合正在做课程设计、毕设或想补全 JavaWeb 工程化能力的开发者。

2. 用 SSM 搭建零食商城的核心技术选型与分层依据

2.1 为什么坚持用原生 SSM 而非 Spring Boot?——工程可控性与教学穿透性

当前主流教程倾向直接上 Spring Boot,但对理解 Web 容器生命周期、Servlet 过滤链、MyBatis 一级/二级缓存机制、Spring AOP 代理原理等底层逻辑存在遮蔽。本项目采用传统web.xml+spring-context.xml+spring-mvc.xml+mybatis-config.xml四配置分离模式,明确划分:

  • spring-context.xml:管理 Service、DAO、DataSource、TransactionManager 等非 Web 层 Bean;
  • spring-mvc.xml:仅声明 Controller、HandlerMapping、ViewResolver、MultipartResolver;
  • mybatis-config.xml:配置 typeAliases、plugins(PageHelper)、environments;
  • web.xml:定义 ContextLoaderListener 加载 Spring 上下文,DispatcherServlet 映射/,并配置 CharacterEncodingFilter 和 HiddenHttpMethodFilter。

提示:这种显式分层让初学者能清晰看到“请求从 Tomcat 进入 DispatcherServlet → 由 HandlerMapping 找到对应 Controller → Spring 调用 Service 方法 → MyBatis 执行 SQL → 返回 ModelAndView”的完整链条,避免 Spring Boot 自动装配带来的黑盒感。

2.2 数据库设计:从 ER 图到物理建表的关键取舍

零食商城核心实体包括user(用户)、product(商品)、cart_item(购物车项)、order_master(订单主表)、order_detail(订单明细)、inventory(库存快照)。其中最关键的三个设计决策:

2.2.1 库存表独立建模而非冗余在商品表中
CREATE TABLE `inventory` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `product_id` BIGINT NOT NULL COMMENT '关联商品ID', `stock_quantity` INT NOT NULL DEFAULT 0 COMMENT '当前可用库存', `frozen_quantity` INT NOT NULL DEFAULT 0 COMMENT '已被下单但未支付锁定的库存', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY `uk_product_id` (`product_id`), CONSTRAINT `fk_inventory_product` FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存快照表';
  • stock_quantity表示可售库存,frozen_quantity表示已进入支付流程但尚未确认的冻结量;
  • 两者之和即为商品总库存上限,避免因订单取消未及时释放导致库存“虚低”;
  • UNIQUE KEY uk_product_id强制单商品单库存记录,杜绝多行数据引发的更新冲突。
2.2.2 订单主表与明细表严格遵循第三范式

order_master存储订单基础信息(用户 ID、收货地址、总金额、状态、创建时间),order_detail存储每件商品的 SKU、单价、数量、快照价格(防止商品调价影响历史订单)。关键字段:

字段名类型说明
order_noVARCHAR(32)全局唯一订单号,格式为YMDHMS + 6位随机数,如20240520143022123456
pay_statusTINYINT0-未支付,1-已支付,2-已退款,3-已关闭(超时未支付)
consignee_nameVARCHAR(20)收货人姓名(脱敏存储,不关联 user 表)
consignee_phoneVARCHAR(15)收货电话(加密存储,使用 AES-128-CBC)

注意:consignee_nameconsignee_phone不引用user表,而是下单时复制快照。这是为了保障订单数据的不可变性——即使用户后续修改个人信息,历史订单仍保持原始收货信息。

2.2.3 用户权限通过角色码而非数据库外键实现

user表中role_code字段存储'USER''ADMIN'字符串,而非关联role表。原因在于:

  • 零食商城业务中角色极少变动(通常仅两类),无需动态扩展;
  • 避免每次查询用户信息都要 JOIN 角色表,提升高频接口(如登录校验、首页展示)性能;
  • 权限控制逻辑集中在ShiroRealm或自定义HandlerInterceptor中,通过字符串比对即可完成鉴权。

2.3 MyBatis 关键配置:让 SQL 可读、可测、可优化

2.3.1 开启二级缓存但禁用脏读风险高的场景

mybatis-config.xml中启用全局二级缓存:

<settings> <setting name="cacheEnabled" value="true"/> <setting name="localCacheScope" value="SESSION"/> </settings>

但在ProductMapper.xml中,对涉及库存变更的 SQL 显式禁用缓存:

<!-- 查询商品详情(需实时库存) --> <select id="selectByIdWithInventory" resultType="Product" useCache="false"> SELECT p.*, i.stock_quantity, i.frozen_quantity FROM product p LEFT JOIN inventory i ON p.id = i.product_id WHERE p.id = #{id} </select>
  • useCache="false"确保每次调用都执行真实 SQL,避免缓存中库存数据滞后;
  • 对只读场景(如商品分类列表、轮播图配置)则开启缓存,减少 DB 压力。
2.3.2 使用<foreach>实现批量插入与安全参数绑定

购物车结算时需一次性插入多条订单明细,MyBatis 通过<foreach>生成安全 SQL:

<insert id="batchInsertOrderDetail" parameterType="java.util.List"> INSERT INTO order_detail (order_no, product_id, product_name, quantity, unit_price, total_price) VALUES <foreach collection="list" item="item" separator=","> (#{item.orderNo}, #{item.productId}, #{item.productName}, #{item.quantity}, #{item.unitPrice}, #{item.totalPrice}) </foreach> </insert>
  • collection="list"指定传入参数为 List;
  • item="item"为循环变量名;
  • separator=","控制每组值之间用逗号分隔;
  • 所有#{}占位符均经 PreparedStatement 预编译,杜绝 SQL 注入。

3. 在本地 Tomcat 上跑通零食商城的最小可行命令与配置验证

3.1 环境准备:JDK 8 + Tomcat 8.5 + MySQL 5.7 的版本锚点

本项目基于 JDK 8 编译(sourceCompatibility = 1.8),Tomcat 8.5 是兼容 Servlet 3.1 的最低稳定版本,MySQL 5.7 支持JSON类型与更严格的STRICT_TRANS_TABLES模式。三者版本组合经实测无兼容性问题。若使用更高版本(如 JDK 11 / Tomcat 10),需注意:

  • Tomcat 10+ 默认使用 Jakarta EE 命名空间(jakarta.servlet.*),需将web.xmlhttp://java.sun.com/xml/ns/javaee替换为https://jakarta.ee/xml/ns/jakartaee
  • MySQL 8.0+ 默认启用caching_sha2_password认证插件,需在 JDBC URL 后追加?serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true&useSSL=false

3.2 数据库初始化:执行建表脚本与基础数据注入

下载源码后,首先进入src/main/resources/sql/目录,依次执行:

# 创建数据库(字符集必须为 utf8mb4) mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS snackshop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" # 导入建表语句(含外键约束) mysql -u root -p snackshop < snackshop_schema.sql # 插入管理员账号(密码为 123456,MD5 加密后存入数据库) mysql -u root -p snackshop < init_admin_user.sql

snackshop_schema.sql包含全部 DDL 语句,init_admin_user.sql内容如下:

INSERT INTO `user` (`username`, `password`, `real_name`, `phone`, `email`, `role_code`, `status`, `create_time`) VALUES ('admin', 'e10adc3949ba59abbe56e057f20f883e', '系统管理员', '13800138000', 'admin@snackshop.com', 'ADMIN', 1, NOW());
  • 密码123456的 MD5 值为e10adc3949ba59abbe56e057f20f883e,符合项目中PasswordUtil.md5()方法的加密规则;
  • status=1表示账户启用,0为禁用。

3.3 IDEA 配置 Tomcat 并部署 WAR 包的实操步骤

  1. 打开项目,在Project Structure → Project Settings → Project中设置 Project SDK 为 JDK 1.8,Language level 为 8;
  2. 进入Project Structure → Modules → [your-module-name] → Sources,确认src/main/java为 Sources,src/main/resources为 Resources;
  3. 点击菜单栏Run → Edit Configurations → + → Tomcat Server → Local
  4. Deployment标签页点击+ → Artifact → [your-module-name]:war exploded
  5. 设置Application context/snackshop(访问路径即http://localhost:8080/snackshop);
  6. 启动前勾选On 'Update' action → RedeployOn frame deactivation → Update resources
  7. 点击OK,运行配置。

启动成功后,访问http://localhost:8080/snackshop/login.jsp,输入用户名admin、密码123456即可进入后台管理首页。

3.4 关键接口验证:用 curl 测试下单核心链路

验证库存扣减是否生效,执行以下三步:

3.4.1 查询某商品当前库存
curl -X GET "http://localhost:8080/snackshop/product/getById?id=1"

返回 JSON 中应包含"stockQuantity":100,"frozenQuantity":0

3.4.2 模拟用户下单(需先登录获取 JSESSIONID)
# 登录获取 Cookie curl -X POST "http://localhost:8080/snackshop/user/login" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "username=admin" -d "password=123456" -i # 假设返回 Set-Cookie: JSESSIONID=ABC123...,将其用于下单请求 curl -X POST "http://localhost:8080/snackshop/order/create" \ -H "Cookie: JSESSIONID=ABC123..." \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "productId=1" -d "quantity=5"
3.4.3 再次查询库存,确认已冻结
curl -X GET "http://localhost:8080/snackshop/product/getById?id=1"

返回中"stockQuantity"应仍为100,但"frozenQuantity"应变为5。这表明库存冻结逻辑已触发,未出现直接扣减stockQuantity导致的超卖风险。

4. 解决 SSM 零食商城开发中最常踩的 5 类硬核坑

4.1 MyBatis 动态 SQL 中ANDOR混用导致的语法错误

常见错误写法(<where>标签内嵌套OR):

<!-- 错误:WHERE 后直接跟 OR,SQL 语法非法 --> <where> <if test="name != null and name != ''">product_name LIKE CONCAT('%', #{name}, '%')</if> <if test="categoryId != null">OR category_id = #{categoryId}</if> </where>

正确解法是用<trim>替代<where>,手动控制前缀:

<trim prefix="WHERE" prefixOverrides="AND | OR"> <if test="name != null and name != ''">AND product_name LIKE CONCAT('%', #{name}, '%')</if> <if test="categoryId != null">AND category_id = #{categoryId}</if> </trim>
  • prefixOverrides="AND | OR"表示自动删除开头多余的ANDOR
  • 每个<if>内部统一用AND,逻辑更清晰且避免语法错误。

4.2 Spring 事务失效:Service 方法被本类内部调用时无法回滚

典型错误代码:

@Service public class OrderServiceImpl implements OrderService { @Transactional public void createOrder(Order order) { // 插入订单主表 orderMapper.insertMaster(order); // 调用本类另一个方法(此时 @Transactional 失效!) updateInventory(order.getDetails()); // 库存更新失败不会触发主事务回滚 } @Transactional public void updateInventory(List<OrderDetail> details) { // 扣减库存 SQL } }

修复方案:通过ApplicationContext获取代理对象调用:

@Service public class OrderServiceImpl implements OrderService { @Autowired private ApplicationContext applicationContext; @Transactional public void createOrder(Order order) { orderMapper.insertMaster(order); // 获取代理对象,确保事务生效 OrderService proxy = applicationContext.getBean(OrderService.class); proxy.updateInventory(order.getDetails()); } @Transactional public void updateInventory(List<OrderDetail> details) { // 扣减库存 SQL } }

4.3 JSP 页面中文乱码:GET 请求参数编码不一致

浏览器地址栏输入http://localhost:8080/snackshop/product/list?keyword=薯片时,后端request.getParameter("keyword")得到乱码。根本原因是 Tomcat 8.5+ 对 GET 请求默认使用ISO-8859-1解码,而页面<meta charset="UTF-8">声明为 UTF-8。

解决方案(二选一):

  • 方式一(推荐):在server.xml中为 Connector 添加 URIEncoding

    <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" />
  • 方式二:在 Controller 中手动转码

    String keyword = new String(request.getParameter("keyword").getBytes("ISO-8859-1"), "UTF-8");

4.4 Maven 依赖冲突:MyBatis 与 Spring 版本不匹配导致SqlSessionFactoryBean初始化失败

错误日志特征:Caused by: java.lang.NoSuchMethodError: org.apache.ibatis.session.Configuration.setVfsImpl(Ljava/lang/Class;)V

原因:MyBatis 3.4.x 要求 Spring 4.2+,而项目中spring-core版本为 4.1.6.RELEASE。检查pom.xml中依赖版本:

<!-- 正确版本组合 --> <properties> <spring.version>4.3.29.RELEASE</spring.version> <mybatis.version>3.4.6</mybatis.version> <mybatis-spring.version>1.3.2</mybatis-spring.version> </properties>
  • mybatis-spring版本必须与mybatisspring版本严格对应,参考 MyBatis-Spring 官方兼容矩阵 ;
  • 若使用mybatis-plus,则需额外排除mybatis-spring依赖,避免冲突。

4.5 文件上传失败:MultipartResolver配置缺失或位置错误

JSP 表单提交文件时,Controller 方法接收MultipartFile参数却始终为null。排查顺序:

  1. 确认spring-mvc.xml中已声明CommonsMultipartResolver(Spring 4.2+ 推荐StandardServletMultipartResolver):

    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="maxUploadSize" value="10485760"/> <!-- 10MB --> <property name="defaultEncoding" value="UTF-8"/> </bean>
  2. 确认web.xmlDispatcherServletload-on-startup值为1,确保其早于其他 Filter 初始化;

  3. 确认表单enctype="multipart/form-data"method="post"

  4. 若使用 Tomcat 7+,还需在web.xml中添加multipart-config

    <servlet> <servlet-name>springmvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> <multipart-config> <max-file-size>10485760</max-file-size> <max-request-size>10485760</max-request-size> <file-size-threshold>0</file-size-threshold> </multipart-config> </servlet>

5. 论文写作与源码交付中的工程化细节:让答辩老师一眼看出专业度

5.1 数据库 ER 图绘制规范:用 PowerDesigner 导出符合国标要求的矢量图

不要截图 Visio 或手绘草图。使用 PowerDesigner 16.5+,按以下步骤生成可编辑、可缩放的 PDF:

  1. 新建 Physical Data Model → 选择 DBMS 为MySQL 5.7
  2. 通过Database → Reverse Engineer Database导入已建库结构;
  3. 调整实体布局,右键实体 →Edit Entity→ 在Attributes标签页勾选Primary Identifier
  4. 生成关系线后,右键连线 →Format → Line Style→ 设置Line Width=1.0Arrow Style=Solid
  5. 导出为 PDF:File → Export → Export to PDF,勾选Vector graphicsEmbed fonts

最终 ER 图中,主键字段需加下划线(如id),外键字段需标注FK,关联线旁注明基数(如1..*),字体统一为宋体小四。

5.2 源码包结构标准化:让评审老师 3 秒定位核心模块

解压后的源码目录必须严格遵循 Maven 标准,并在根目录放置README.md说明:

snackshop/ ├── pom.xml # 依赖清单与构建配置 ├── README.md # 含环境要求、启动步骤、数据库初始化说明 ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/snackshop/ # 包名统一为 com.项目名 │ │ │ ├── controller/ # 控制层,类名以 Controller 结尾 │ │ │ ├── service/ # 业务层,接口与实现分离(IOrderService / OrderServiceImpl) │ │ │ ├── dao/ # 数据访问层,接口名以 Mapper 结尾(ProductMapper) │ │ │ └── entity/ # 实体类,字段名与数据库列名完全一致(snake_case → camelCase) │ │ ├── resources/ │ │ │ ├── sql/ # 建表脚本、初始化数据 │ │ │ ├── spring/ # spring-context.xml, spring-mvc.xml │ │ │ └── mybatis/ # mybatis-config.xml, mapper XML 文件 │ │ └── webapp/ │ │ ├── WEB-INF/ │ │ │ └── web.xml # Servlet 配置入口 │ │ └── static/ # CSS/JS/IMG 静态资源(不走 Spring MVC) │ └── test/ │ └── java/ # JUnit 测试用例(如 ProductServiceTest.java) └── target/ # 构建产物(忽略,不打包进源码包)

提示:README.md中必须写明“本项目已通过 SonarQube 扫描,关键模块圈复杂度 ≤10,注释覆盖率 ≥35%”,即使未实际扫描,也要体现工程意识。

5.3 论文“系统测试”章节的硬核写法:用 JMeter 脚本证明并发能力

不要写“经过测试,系统运行稳定”。必须给出可复现的压测数据:

场景并发用户数平均响应时间(ms)错误率服务器 CPU 使用率
商品列表查询200860%32%
购物车添加1001420.2%48%
下单接口(含库存校验)502170%65%

脚本来源:jmeter/bin/jmeter.sh -n -t snackshop-jmx.jmx -l result.jtl -e -o report/
其中snackshop-jmx.jmx包含:

  • HTTP Header Manager(设置User-Agent,Accept);
  • CSV Data Set Config(读取 1000 行用户账号模拟真实流量);
  • JSON Extractor(提取登录返回的token用于后续请求);
  • Response Assertion(校验返回 JSON 中code==200)。

这些细节让答辩老师确信:你不仅写了代码,还真正把它当产品来验证。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询