☰
SSM框架实战:超市商品信息管理系统毕业设计全流程指南
2026/10/13 12:38:36 网站建设 项目流程

简介:这份资源是面向计算机专业毕业设计场景的SSM超市商品信息管理系统完整程序包,适合正在准备Java方向毕设的本科生及需要参考SSM框架实战项目的开发者。系统基于Java技术栈构建,开发过程涵盖需求分析、总体设计与详细设计,涉及系统功能设计、总体结构设计、数据结构设计及安全设计,并完成数据库访问实现与各功能模块的关键代码编写,最后进行功能测试与结果分析。压缩包共653个文件,约10.57MB,以134个java源码、76个vue与76个html页面、62个js脚本、82张jpg与75张gif界面素材为主,另含26个xml配置、19个css样式、2个sql数据库脚本及class、properties等编译与配置文件,目录结构完整。已有837人学习下载。资源包含毕设程序源代码一份与数据库一份,可完美运行,配置环境说明齐全,能帮助读者快速理解SSM分层架构、数据库访问与模块实现思路,适合作为毕设参考与排错对照。

1. 从零手搓一套超市商品信息管理系统:毕业设计选 SSM 到底图什么

很多同学做毕业设计时,第一反应是去搜“ssm超市商品信息管理系统毕业设计程序”,然后被一堆压缩包和代做广告淹没。我当年也踩过这个坑,后来带过几届学弟学妹,发现真正能让你顺利过答辩、还能在简历上写一笔的,不是代码有多花哨,而是架构选得稳、业务闭环完整。SSM(Spring + SpringMVC + MyBatis)这套组合虽然被调侃为“老古董”,但它对理解 Java Web 的分层思想、事务控制和 MVC 请求流转,依然是性价比最高的选择。这套系统要解决的核心问题很具体:商品录入、分类管理、库存预警、订单关联查询,以及一个能演示权限隔离的后台。适合谁?适合刚学完 Java Web、想用一个完整项目把知识点串起来,并且需要一份能讲清楚“为什么这么设计”的毕业设计底稿的人。别急着复制粘贴,先想明白每一层在干什么,后面改需求才不会翻车。

2. 环境搭建与数据库设计:别让版本冲突成为第一个拦路虎

2.1 用 Maven 锁定依赖版本,避开 Jar 包玄学

SSM 项目最烦人的就是版本不兼容。我一般会直接建一个 Maven 项目,在pom.xml里把 Spring、SpringMVC、MyBatis 以及数据库驱动、连接池的版本号写死。别用最新版,选一套经过验证的稳定组合,比如 Spring 5.3.x + MyBatis 3.5.x + MySQL 8.0 驱动。下面是我常用的依赖配置片段,注意mysql-connector-java的版本要和你的数据库服务端匹配,否则会报时区或 SSL 连接错误。

<!-- pom.xml 核心依赖节选 --> <dependencies> <!-- Spring 核心上下文与事务 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.30</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.3.30</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.3.30</version> </dependency> <!-- SpringMVC 前端控制器 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.30</version> </dependency> <!-- MyBatis 与 Spring 整合包 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.1.2</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.13</version> </dependency> <!-- MySQL 驱动,注意与本地数据库版本对应 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- Druid 连接池,监控 SQL 执行情况 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.20</version> </dependency> <!-- JSP 与 JSTL,用于后台页面渲染 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies>

逻辑说明:Spring 的spring-tx和spring-jdbc是后面配声明式事务的基础,MyBatis 的mybatis-spring负责把 SqlSession 交给 Spring 管理。参数上,mysql-connector-java用 8.x 时,JDBC URL 必须带serverTimezone=Asia/Shanghai,否则启动就抛时区异常。Druid 连接池不是必须,但它自带监控页面,调 SQL 性能时能省很多事。

2.2 商品表与库存表的字段设计,留好扩展位

数据库设计直接决定后面业务代码好不好写。超市商品信息管理系统的核心表至少要有:商品分类表、商品信息表、库存记录表、供应商表(可选)、用户表。我见过很多同学把商品名称、分类、价格、库存全塞一张表,结果做分类筛选时只能写死枚举,后期加一个分类就要改代码。正确的做法是分类独立成表,商品表用外键关联。下面这张表是我常用的商品表结构,注意stock字段和warning_stock字段分开,方便做库存预警。

-- 商品分类表 CREATE TABLE `category` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL COMMENT '分类名称', `sort` int DEFAULT 0 COMMENT '排序权重', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 商品信息表 CREATE TABLE `product` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '商品名称', `category_id` int NOT NULL COMMENT '所属分类', `price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '售价', `cost` decimal(10,2) DEFAULT '0.00' COMMENT '进价', `stock` int NOT NULL DEFAULT '0' COMMENT '当前库存', `warning_stock` int DEFAULT '10' COMMENT '库存预警阈值', `supplier` varchar(100) DEFAULT NULL COMMENT '供应商', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`), CONSTRAINT `fk_product_category` FOREIGN KEY (`category_id`) REFERENCES `category` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

逻辑说明:category_id上建了索引,因为后台列表页大概率要按分类过滤。warning_stock给一个默认值 10,这样新增商品时即使不填也有预警线。外键约束在毕业设计里建议加上,答辩时能体现数据完整性意识,但如果你用的是 MyISAM 引擎或者后期想分库分表,外键就是累赘,所以生产环境常见做法是逻辑外键,这里为了演示清晰保留物理外键。

2.3 用 Druid 配数据源,把连接池参数调明白

数据源配置写在applicationContext.xml里,我一般把数据库连接信息单独抽到jdbc.properties,方便改环境。Druid 的几个关键参数:初始连接数、最大连接数、最小空闲数、超时等待时间。毕业设计并发不高,但参数写合理了,答辩老师会觉得你懂生产意识。

# jdbc.properties jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/supermarket?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=你的密码
<!-- applicationContext.xml 数据源配置 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <!-- 初始化连接数 --> <property name="initialSize" value="5"/> <!-- 最大活跃连接数 --> <property name="maxActive" value="20"/> <!-- 最小空闲连接数 --> <property name="minIdle" value="5"/> <!-- 获取连接超时时间,毫秒 --> <property name="maxWait" value="60000"/> <!-- 心跳检测,防止连接被数据库断开 --> <property name="validationQuery" value="SELECT 1"/> <property name="testWhileIdle" value="true"/> </bean>

逻辑说明:maxActive设 20 对毕业设计足够,设太大反而浪费资源。validationQuery是 Druid 用来检测连接是否有效的 SQL,MySQL 下写SELECT 1即可。testWhileIdle为 true 表示空闲时检测,避免拿到已经被服务端关闭的死连接。注意jdbc.properties需要在 Spring 配置里用<context:property-placeholder>加载,否则${}占位符不生效。

3. 用 MyBatis 打通商品增删改查与分页查询

3.1 实体类与 Mapper 接口的对应关系

MyBatis 的核心是 Mapper 接口和 XML 映射文件。实体类Product的字段要和数据库列名对应,如果列名是下划线风格(如category_id),要么在 MyBatis 配置里开启驼峰映射,要么在 SQL 里用别名。我习惯在mybatis-config.xml里打开mapUnderscoreToCamelCase,这样实体类里写categoryId就能自动映射category_id。

// Product.java 实体类节选 public class Product { private Integer id; private String name; private Integer categoryId; private BigDecimal price; private BigDecimal cost; private Integer stock; private Integer warningStock; private String supplier; private Date createTime; private Date updateTime; // 省略 getter/setter }
<!-- mybatis-config.xml 全局设置 --> <settings> <!-- 开启驼峰命名自动映射 --> <setting name="mapUnderscoreToCamelCase" value="true"/> <!-- 打印 SQL 日志,调试时非常有用 --> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings>

逻辑说明:mapUnderscoreToCamelCase是 MyBatis 最实用的开关之一,省去大量<resultMap>手写映射。logImpl设为STDOUT_LOGGING后,控制台会打印执行的 SQL 和参数,调分页或条件查询时一眼就能看出哪里拼错了。

3.2 商品列表的分页查询:用 PageHelper 还是手写 limit

分页是后台列表的刚需。常见做法有两种:引入 PageHelper 插件,或者手写LIMIT语句。PageHelper 用起来简单,但有些答辩老师会追问原理,手写LIMIT更能体现你对 SQL 的掌控。我一般会手写一个分页查询,同时把总数查询也写清楚,这样在 Service 层组装分页对象。

<!-- ProductMapper.xml 分页查询与总数统计 --> <select id="selectByPage" resultType="Product"> SELECT * FROM product <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> </where> ORDER BY update_time DESC LIMIT #{offset}, #{pageSize} </select> <select id="countByCondition" resultType="int"> SELECT COUNT(*) FROM product <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> </where> </select>

逻辑说明:<where>标签会自动处理第一个AND,避免WHERE AND的语法错误。offset是偏移量,计算公式是(pageNum - 1) * pageSize,这个计算放在 Service 层做,Mapper 只负责接收算好的值。countByCondition的条件必须和selectByPage完全一致,否则分页总数和实际数据对不上,这是新手最容易忽略的坑。

3.3 Service 层事务控制:库存扣减必须加 @Transactional

商品管理不只是增删改查,还要处理库存变动。比如入库时增加库存、销售时扣减库存。这类操作涉及多张表(商品表 + 库存流水表),必须用事务包起来。Spring 的声明式事务在 Service 层方法上加@Transactional即可,但要注意默认只对RuntimeException回滚,如果你抛的是受检异常,得手动指定rollbackFor。

@Service public class ProductServiceImpl implements ProductService { @Autowired private ProductMapper productMapper; @Autowired private StockRecordMapper stockRecordMapper; @Override @Transactional(rollbackFor = Exception.class) public void updateStock(Integer productId, Integer changeNum, String reason) { // 1. 查询当前商品,判断库存是否足够 Product product = productMapper.selectById(productId); if (product == null) { throw new RuntimeException("商品不存在"); } int newStock = product.getStock() + changeNum; if (newStock < 0) { throw new RuntimeException("库存不足,当前库存:" + product.getStock()); } // 2. 更新商品库存 productMapper.updateStock(productId, newStock); // 3. 写入库存流水,便于追溯 StockRecord record = new StockRecord(); record.setProductId(productId); record.setChangeNum(changeNum); record.setReason(reason); record.setCreateTime(new Date()); stockRecordMapper.insert(record); } }

逻辑说明:@Transactional(rollbackFor = Exception.class)确保任何异常都触发回滚,避免库存扣了但流水没记上。updateStock的 SQL 建议写成UPDATE product SET stock = #{newStock} WHERE id = #{productId},不要写成stock = stock + #{changeNum},因为后者在并发下可能覆盖其他操作,虽然毕业设计并发低,但养成先查再改的习惯,答辩时能讲出乐观锁或悲观锁的优化方向。

4. 避坑与排查:SSM 整合中最容易翻车的 5 个地方

4.1 现象:启动报 NoClassDefFoundError 或 ClassNotFoundException

原因:Maven 依赖冲突或 Jar 包没下载完整。SSM 涉及 Spring、MyBatis、Servlet API 等多个库,版本不匹配时经常缺类。解决:在 IDEA 里用mvn dependency:tree查看依赖树,排除重复的commons-logging或javax.servlet-api。另外,Tomcat 运行时已经带了 Servlet API,pom.xml里该依赖的 scope 要设为provided,否则打包进 WEB-INF/lib 会和容器冲突。

4.2 现象:Controller 返回中文乱码

原因:请求或响应编码没统一。GET 请求乱码通常是 Tomcat 的server.xml没配 URIEncoding,POST 请求乱码则是 SpringMVC 的字符编码过滤器没生效。解决:在web.xml里配置CharacterEncodingFilter,强制请求和响应都用 UTF-8,同时把forceEncoding设为 true。数据库连接 URL 里也要带characterEncoding=utf8,三层缺一不可。

<!-- web.xml 编码过滤器 --> <filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

4.3 现象:MyBatis 查询返回 null,但数据库里明明有数据

原因:字段名和实体类属性名没对上,且没开启驼峰映射。比如数据库列category_id,实体类属性categoryId,如果mapUnderscoreToCamelCase没开,MyBatis 就找不到映射。解决:检查mybatis-config.xml的 settings 是否被正确加载,或者在 Mapper XML 里手动写<resultMap>。另一个常见原因是 SQL 里用了#{}但参数名写错,比如接口方法参数是@Param("name") String productName,XML 里却写#{productName},也会导致条件不生效。

4.4 现象:事务加了 @Transactional 但不回滚

原因:Spring 事务默认基于代理,如果 Service 类没有实现接口,或者方法不是 public,代理可能不生效。另外,如果在同一个类里 A 方法调用 B 方法,B 方法的事务注解也会失效。解决:确保 Service 实现类实现了接口,事务方法声明为 public。如果确实需要同类内部调用,可以用AopContext.currentProxy()获取代理对象,或者把事务方法拆到另一个 Service 里。最稳妥的方式是在applicationContext.xml里开启<tx:annotation-driven transaction-manager="transactionManager"/>,并确认事务管理器引用了正确的数据源。

4.5 现象:库存扣减后出现负数

原因:并发下先查后改不是原子操作,或者代码里没做库存校验。解决:在 SQL 的UPDATE语句里加条件AND stock >= #{changeNum},利用数据库行锁保证原子性。如果更新影响行数为 0,说明库存不足,直接抛异常回滚。这样即使多个请求同时进来,也不会把库存扣成负数。毕业设计里可以不用分布式锁,但这条 SQL 条件必须加上,答辩时能体现你对并发安全的基本认知。

5. 进阶技巧:用拦截器做权限校验与操作日志

5.1 登录拦截器的实现与放行规则

后台管理系统必须区分登录用户和游客。用 SpringMVC 的拦截器HandlerInterceptor做统一鉴权,比在每个 Controller 里写 if 判断优雅得多。核心逻辑是:从 Session 里取用户信息,如果为空且请求不是登录页或静态资源,就重定向到登录页。

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录页面和静态资源 String uri = request.getRequestURI(); if (uri.contains("/login") || uri.contains("/css/") || uri.contains("/js/")) { return true; } // 检查 Session 中是否有用户 Object user = request.getSession().getAttribute("loginUser"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login"); return false; } return true; } }

逻辑说明:preHandle返回 false 会中断请求,所以重定向后必须 return false。放行规则里用contains判断 URI 是简单做法,更严谨的方式是用AntPathMatcher匹配/static/**这类模式。拦截器需要在springmvc.xml里注册,并指定拦截路径和排除路径。

<!-- springmvc.xml 拦截器配置 --> <mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login"/> <mvc:exclude-mapping path="/static/**"/> <bean class="com.example.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>

5.2 用 AOP 记录商品操作日志

毕业设计里加一个操作日志功能,能明显提升项目完整度。用 Spring AOP 在 Service 方法执行后自动记录“谁在什么时间修改了哪个商品”。定义一个自定义注解@LogRecord,然后在切面里拦截标注了该注解的方法。

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface LogRecord { String value() default ""; // 操作描述 }
@Aspect @Component public class LogAspect { @Autowired private LogMapper logMapper; @AfterReturning("@annotation(logRecord)") public void afterReturning(JoinPoint joinPoint, LogRecord logRecord) { // 获取方法参数中的商品 ID Object[] args = joinPoint.getArgs(); String methodName = joinPoint.getSignature().getName(); // 从 Session 或 ThreadLocal 中获取当前用户,这里简化处理 String operator = "admin"; LogEntity log = new LogEntity(); log.setOperator(operator); log.setMethod(methodName); log.setDescription(logRecord.value()); log.setCreateTime(new Date()); logMapper.insert(log); } }

逻辑说明:@AfterReturning表示方法正常返回后执行,如果方法抛异常则不记录。@annotation(logRecord)绑定注解实例,可以拿到注解上的描述文字。实际项目中操作人信息通常从 Session 或 Spring Security 上下文获取,这里为了演示方便写死。日志表建议加索引在create_time上,方便按时间范围查询。

5.3 库存预警的定时任务与页面提醒

库存预警是超市系统的实用功能。我一般用 Spring 的@Scheduled注解做一个定时任务,每天凌晨扫描一次库存低于预警线的商品,把结果写入预警表或者直接发邮件。毕业设计里不需要真的发邮件,在后台首页用红色标签展示即可。

@Component public class StockWarningTask { @Autowired private ProductMapper productMapper; // 每天凌晨 2 点执行 @Scheduled(cron = "0 0 2 * * ?") public void checkStock() { List<Product> warningList = productMapper.selectWarningProducts(); // 这里可以将预警列表缓存到 Redis 或写入预警记录表 System.out.println("库存预警商品数量:" + warningList.size()); } }

逻辑说明:cron表达式0 0 2 * * ?表示每天 2:00 触发。需要在 Spring 配置里开启<task:annotation-driven/>。selectWarningProducts的 SQL 条件是WHERE stock <= warning_stock。如果想让页面实时显示,可以在商品列表查询时顺带把预警状态算出来,而不是依赖定时任务。

5.4 用 Postman 做接口自测,别等前端联调才发现问题

很多同学写完 Controller 就直接丢给前端,结果联调时发现参数接不到、返回格式不对。我习惯用 Postman 先把每个接口跑一遍,确认请求参数、响应 JSON 结构、状态码都符合预期。比如商品新增接口,先发一个POST请求,Body 里带 JSON 数据,看数据库是否真的插入了记录。分页接口要测边界值:页码为 1、页码超出总页数、每页条数为 0 或负数。这些边界情况在答辩时被问到,你能说出处理方式,就是加分项。

5.5 打包部署到 Tomcat 的注意事项

最后一步是打包成 WAR 丢进 Tomcat。常见问题是pom.xml里打包方式没写<packaging>war</packaging>,或者maven-war-plugin配置缺失导致 web.xml 找不到。另外,JDK 版本要和 Tomcat 运行的 JDK 一致,Spring 5.x 最低要求 JDK 8。部署后如果报 404,先看 Tomcat 日志里应用是否启动成功,再看contextPath是否和访问 URL 对得上。我一般会把日志级别调到 DEBUG,确认 Spring 容器加载了所有 Bean,再改成 INFO 上线。

希望帮到你。

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

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

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

立即咨询