news 2026/11/5 13:17:52

SSM超市管理系统实战:从课设跑通到二次开发全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SSM超市管理系统实战:从课设跑通到二次开发全解析

简介:这份资源是面向高校计算机相关专业学生与Java入门开发者的SSM课程设计参考项目,围绕超市管理场景,解决商品、库存、销售等业务模块从设计到落地的实践需求,适合作为课程作业、毕业设计原型或SSM框架练手案例。压缩包共792个文件,约1.78MB,以452个html页面、41个js脚本、16个css样式构成前端视图层,48个java源文件与48个class编译文件对应后端控制器与实体类,另有28个xml配置、12个jsp页面及1个sql建库脚本,完整覆盖Spring依赖注入与事务管理、Spring MVC请求分发、MyBatis数据库映射的开发链路。已有86人学习下载。项目按商品管理、库存出入库与盘点、销售订单与报表、会员积分及统计报表等模块组织,目录结构清晰,读者可据此理解分层架构、掌握控制器与实体类协作方式,并借助SQL脚本快速还原数据库,为二次开发与答辩演示提供可运行的参考骨架。

1. 从一份“能跑起来的课设”说起:SSM 超市管理系统到底解决了什么

每年到了课程设计季,后台总有人问同一个问题:有没有一个结构完整、能直接跑、还能改的 SSM 项目?答案往往让人失望——网上流传的很多“超市管理系统”要么只有几张截图,要么代码跑不起来,要么把 Spring MVC、MyBatis 的配置写得云里雾里。这份基于 SSM 的超市管理系统,恰好是那种“下载下来、改个数据库连接、就能登录进后台”的完整工程。它把商品管理、库存出入库、供应商维护、收银下单、订单查询这几条超市业务主线串了起来,前端用 JSP 加少量 JavaScript 做页面渲染和表单校验,后端用 Spring MVC 接请求、Spring 管 Bean 和事务、MyBatis 负责 SQL 映射。适合两类人:一类是刚学完 SSM 三件套、想找一个真实业务场景练手的同学;另一类是带课设、需要一份可讲解、可拆改的参考工程的开发者。下面我按“这项目怎么组织、怎么跑、怎么改”的顺序,把它拆开讲清楚。

2. 工程结构与技术栈:一个请求从浏览器到数据库的完整链路

2.1 分层结构与目录约定

这个项目是典型的 Maven Web 工程,打包方式是 war,最终丢进 Tomcat 运行。目录遵循 Maven 标准布局,理解这套约定比记住每个类名更重要,因为后面改代码、加模块都靠它定位。

supermarket/ ├── pom.xml └── src/main/ ├── java/com/shop/ │ ├── controller/ # Spring MVC 控制器,接收前端请求 │ ├── service/ # 业务接口 │ ├── service/impl/ # 业务实现,事务边界在这里 │ ├── mapper/ # MyBatis Mapper 接口 │ ├── entity/ # 实体类,与数据库表对应 │ └── util/ # 分页、日期、MD5 等工具 ├── resources/ │ ├── spring-mvc.xml # 视图解析、静态资源、拦截器 │ ├── spring-service.xml # 扫描 service、事务管理器 │ ├── spring-mybatis.xml # 数据源、SqlSessionFactory │ ├── mybatis/ # Mapper XML 与 mybatis-config │ └── jdbc.properties # 数据库连接参数 └── webapp/ ├── WEB-INF/ │ ├── web.xml # 前端控制器、编码过滤器 │ └── views/ # JSP 页面 └── static/ # css、js、图片

分层职责很清晰:controller 只做参数接收和视图跳转,不写业务判断;service 承担业务规则和事务;mapper 只负责 SQL。这种“薄 controller、厚 service”的写法在课设里经常被写反,导致 controller 里塞满 if-else,后面想加一个库存校验就得改三处。这个工程把库存扣减、订单状态流转都放在 service 层,改起来只动一个地方。

2.2 三大配置文件各自管什么

SSM 项目最容易让人翻车的地方就是配置文件分散、职责交叉。这个工程把配置拆成三份,每份只干一件事,读的时候按“数据源 → 业务 → 视图”的顺序看就不会乱。

<!-- spring-mybatis.xml:只负责数据源和 SqlSessionFactory --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mybatis/*.xml"/> <property name="typeAliasesPackage" value="com.shop.entity"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.shop.mapper"/> </bean>

这段配置做了三件事:用 Druid 建连接池、告诉 MyBatis 去哪里找 Mapper XML、把com.shop.mapper下的接口自动注册成 Bean。mapperLocations写的是classpath:mybatis/*.xml,意味着新增一个 Mapper XML 只要放进这个目录就会被加载,不用改配置。typeAliasesPackage让 XML 里可以直接写Product而不用写全限定名,这是减少 XML 冗余的常见做法。

<!-- spring-service.xml:事务和 service 扫描 --> <context:component-scan base-package="com.shop.service"/> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/>

事务管理器绑定同一个数据源,开启注解驱动后,service 方法上加@Transactional就能生效。注意这里扫描的是com.shop.service,如果 service 实现类放在别的包下,事务不会生效——这是后面避坑章节要展开的一个高频问题。

<!-- spring-mvc.xml:视图和静态资源 --> <context:component-scan base-package="com.shop.controller"/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean> <mvc:resources mapping="/static/**" location="/static/"/> <mvc:annotation-driven/>

视图解析器把 controller 返回的"product/list"拼成/WEB-INF/views/product/list.jsp,放在 WEB-INF 下是为了防止用户直接通过 URL 访问 JSP。静态资源单独放行,否则会被 DispatcherServlet 拦截导致 css、js 加载 404。

2.3 web.xml 里的两个关键入口

web.xml是整个 Web 应用的启动入口,这个工程里真正需要关注的是编码过滤器和前端控制器。

<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> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>

forceEncoding=true保证请求和响应都用 UTF-8,中文商品名、供应商名不会变成乱码。url-pattern写成/而不是/*,前者只拦截动态请求、放行静态资源,后者会把静态资源也吞掉。这两个细节在课设答辩时经常被问到,也是新手最容易抄错的地方。

3. 核心业务模块拆解:商品、库存与订单怎么串起来

3.1 商品管理:一个标准 CRUD 的完整写法

商品管理是整个系统的地基,其他模块都围绕商品 ID 展开。先看 controller,它只做三件事:接参数、调 service、返回视图或重定向。

@Controller @RequestMapping("/product") public class ProductController { @Autowired private ProductService productService; // 分页查询,page 从 1 开始,size 默认 10 @RequestMapping("/list") public String list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(required = false) String keyword, Model model) { PageResult<Product> result = productService.page(page, 10, keyword); model.addAttribute("page", result); model.addAttribute("keyword", keyword); return "product/list"; } // 新增商品,成功后重定向到列表,避免刷新重复提交 @RequestMapping(value = "/add", method = RequestMethod.POST) public String add(Product product, RedirectAttributes ra) { productService.save(product); ra.addFlashAttribute("msg", "新增成功"); return "redirect:/product/list"; } }

@RequestParam(defaultValue = "1")让分页参数缺省时不会报错,required = false的 keyword 支持按名称模糊搜索。新增用 POST 加重定向,是防止浏览器刷新时重复插入的常规手段,RedirectAttributes把提示信息带到下一个请求,页面刷新后提示消失,不会一直挂着。

service 层负责业务校验和事务:

@Service public class ProductServiceImpl implements ProductService { @Autowired private ProductMapper productMapper; @Override @Transactional public void save(Product product) { // 商品编码唯一性校验,重复直接抛业务异常 if (productMapper.countByCode(product.getCode()) > 0) { throw new BizException("商品编码已存在"); } product.setCreateTime(new Date()); productMapper.insert(product); } @Override public PageResult<Product> page(int page, int size, String keyword) { int offset = (page - 1) * size; List<Product> rows = productMapper.selectPage(offset, size, keyword); int total = productMapper.count(keyword); return new PageResult<>(rows, total, page, size); } }

@Transactional加在 save 上,唯一性校验和插入在同一个事务里,虽然这里只有一条写操作,但保持习惯很重要——后面库存扣减涉及多表写入时,事务边界就是靠这一层控制的。分页没有用 PageHelper 插件,而是手写 offset 和 count,好处是 SQL 完全可控,课设答辩时能讲清楚每一行在干什么。

Mapper XML 对应两条 SQL:

<select id="selectPage" resultType="Product"> SELECT id, code, name, price, stock, supplier_id AS supplierId FROM product <where> <if test="keyword != null and keyword != ''"> AND name LIKE CONCAT('%', #{keyword}, '%') </if> </where> ORDER BY id DESC LIMIT #{offset}, #{size} </select> <select id="count" resultType="int"> SELECT COUNT(*) FROM product <where> <if test="keyword != null and keyword != ''"> AND name LIKE CONCAT('%', #{keyword}, '%') </if> </where> </select>

<where>标签会自动处理开头的 AND,<if>做动态条件拼接,#{}是预编译占位符,能防 SQL 注入。supplier_id AS supplierId是为了让下划线字段映射到驼峰属性,如果全局开了mapUnderscoreToCamelCase,这行可以省掉,但显式写出来更直观。

3.2 库存出入库:多表写入与事务控制

库存是这个系统里唯一涉及“数量变化”的模块,也是最容易出并发问题的地方。工程里的做法是:每次出入库都写一条流水记录,同时更新商品表的库存字段,两步放在同一个事务里。

@Service public class StockServiceImpl implements StockService { @Autowired private StockMapper stockMapper; @Autowired private ProductMapper productMapper; @Override @Transactional(rollbackFor = Exception.class) public void stockIn(Long productId, int num, String operator) { if (num <= 0) { throw new BizException("入库数量必须大于 0"); } // 先更新库存,再写流水,顺序反了会导致流水有记录但库存没加 int updated = productMapper.increaseStock(productId, num); if (updated == 0) { throw new BizException("商品不存在"); } StockRecord record = new StockRecord(); record.setProductId(productId); record.setType("IN"); record.setNum(num); record.setOperator(operator); record.setCreateTime(new Date()); stockMapper.insert(record); } }

rollbackFor = Exception.class是关键,Spring 默认只对 RuntimeException 回滚,业务里抛的受检异常不会触发回滚,加上这个参数才保险。更新库存用increaseStock这种带条件的 UPDATE,而不是先查再改,避免并发下两个请求读到同一个旧值。

<update id="increaseStock"> UPDATE product SET stock = stock + #{num} WHERE id = #{productId} </update> <update id="decreaseStock"> UPDATE product SET stock = stock - #{num} WHERE id = #{productId} AND stock >= #{num} </update>

出库的 SQL 带了AND stock >= #{num},库存不足时更新影响行数为 0,service 层据此抛异常回滚。这种“把校验写进 SQL 条件里”的做法,比先查库存再判断更可靠,因为查和改之间没有间隙。代价是错误提示不够细,只能告诉用户“库存不足或商品不存在”,但对课设场景够用了。

3.3 收银下单:订单主从表的一次性写入

下单是业务链路最长的一环:校验库存、生成订单主表、写入订单明细、扣减库存,四步必须原子完成。

@Transactional(rollbackFor = Exception.class) public Long createOrder(Long userId, List<OrderItem> items) { if (items == null || items.isEmpty()) { throw new BizException("订单不能为空"); } BigDecimal total = BigDecimal.ZERO; for (OrderItem item : items) { Product p = productMapper.selectById(item.getProductId()); if (p == null) { throw new BizException("商品不存在:" + item.getProductId()); } // 单价以数据库为准,不信任前端传值 item.setPrice(p.getPrice()); total = total.add(p.getPrice().multiply(new BigDecimal(item.getNum()))); } Order order = new Order(); order.setUserId(userId); order.setTotalAmount(total); order.setStatus("PAID"); order.setCreateTime(new Date()); orderMapper.insert(order); // 主表,回填自增 id for (OrderItem item : items) { item.setOrderId(order.getId()); orderItemMapper.insert(item); int updated = productMapper.decreaseStock(item.getProductId(), item.getNum()); if (updated == 0) { throw new BizException("库存不足:" + item.getProductId()); } } return order.getId(); }

单价从数据库重新取,是防止前端篡改价格;订单主表插入后 MyBatis 的useGeneratedKeys会把自增 ID 回填到 order 对象,明细表才能拿到 orderId。任何一步失败,整个方法回滚,不会出现“订单生成了但库存没扣”的脏数据。这段代码是整个项目里最值得反复读的部分,二次开发加优惠券、加积分,都是在这个骨架上挂逻辑。

4. 把项目跑起来:环境、建库与启动排查

4.1 环境准备与依赖版本

这个工程对版本比较敏感,Spring 4.x 配 MyBatis 3.4 左右是稳妥组合,JDK 用 8,Tomcat 用 8.5 或 9。版本错配是启动失败的头号原因,先把这张表对一遍再动手。

组件建议版本说明
JDK1.8工程用了 lambda 和部分 8 的语法
Maven3.6+负责依赖下载和 war 打包
Tomcat8.5 / 9.0不要用 10,包名已变成 jakarta
MySQL5.7 / 8.08.0 需注意驱动类和时区参数
Spring4.3.x与 MyBatis 整合稳定
MyBatis3.4.x与 spring-mybatis 1.3.x 配套

pom.xml里重点看三块依赖:spring-webmvc、mybatis-spring、mysql-connector-java,以及 druid 连接池。如果本地仓库拉不下来,换一个可用的镜像源,不要手动往 lib 里塞 jar,war 打包时会漏。

4.2 建库与连接配置

数据库脚本一般在src/main/resources/sql/下,先建库再导表:

mysql -u root -p -e "CREATE DATABASE shop DEFAULT CHARSET utf8mb4;" mysql -u root -p shop < src/main/resources/sql/shop.sql

然后改jdbc.properties:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=你的密码

MySQL 8 的驱动类是com.mysql.cj.jdbc.Driver,5.7 用com.mysql.jdbc.Driver;serverTimezone不写会在启动时报时区错误,这是血泪经验里最常见的一条。useSSL=false只是本地开发图省事,生产环境不要照抄。

4.3 启动方式与验证路径

用 IDEA 的话,配一个 Tomcat Server,Deployment 里加 war exploded,Application context 设成/shop,启动后访问http://localhost:8080/shop/login。用命令行则是:

mvn clean package -DskipTests cp target/supermarket.war $TOMCAT_HOME/webapps/ $TOMCAT_HOME/bin/startup.sh

启动成功的标志是控制台没有BeanCreationException,访问登录页能看到页面而不是 404 或 500。登录后按“商品列表 → 新增商品 → 入库 → 下单”走一遍,能完整走通说明数据源、事务、视图解析都正常。如果卡在某一步,先看 Tomcat 日志里第一个异常,SSM 的报错往往是连锁的,第一个才是根因。

5. 避坑与排查:那些让课设卡到半夜的问题

5.1 事务不生效,库存扣了流水没写

现象:下单后订单生成了,但库存没减,或者流水表少一条。原因通常是@Transactional加在了 controller 上,或者 service 类没有被 Spring 扫描到,又或者方法内部this.调用导致代理失效。解决:确认注解在 service 实现类的方法上,确认spring-service.xml的扫描包覆盖到实现类,跨方法调用通过注入的接口走代理,不要用this.createOrder()自调用。

5.2 中文乱码,商品名变成问号

现象:页面输入中文,存进数据库变成???。原因有三层:数据库字符集不是 utf8mb4、连接串没带characterEncoding=utf8、web.xml 没配编码过滤器。解决:建库时指定DEFAULT CHARSET utf8mb4,连接串补上编码参数,过滤器设forceEncoding=true,三处缺一不可。改完记得重启 Tomcat,热部署不一定重新加载过滤器。

5.3 静态资源 404,页面没样式

现象:JSP 能打开,但 css、js 全部 404。原因是 DispatcherServlet 映射成/后接管了所有请求,静态资源也被当成 controller 去找。解决:在spring-mvc.xml里加<mvc:resources mapping="/static/**" location="/static/"/>,同时确认web.xml的 url-pattern 是/不是/*。如果用了拦截器做登录校验,记得在拦截器配置里排除/static/**。

5.4 Mapper 绑定失败,启动报 Invalid bound statement

现象:启动时报Invalid bound statement (not found),或者调用时提示找不到 SQL。原因:Mapper XML 的 namespace 和接口全限定名不一致、XML 没放在mapperLocations能扫到的目录、或者方法名和 XML 的 id 对不上。解决:namespace 写成com.shop.mapper.ProductMapper,XML 放resources/mybatis/下,方法名和 id 逐字核对。改完执行mvn clean,target 里的旧 XML 不清掉会继续报错。

5.5 分页参数越界,第二页空白

现象:第一页正常,点第二页列表为空。原因:page 参数从 0 开始传,而 SQL 的 offset 按(page-1)*size算,导致 offset 为负或错位。解决:统一约定 page 从 1 开始,controller 里对 page 做下限保护,page = Math.max(page, 1),并在 PageResult 里算好总页数,前端翻页按钮根据总页数禁用越界页码。

6. 二次开发怎么改:从加一个字段到加一个模块

拿到这种课设工程,最常见的改造需求是“加字段、加模块、换前端”。我的习惯是先跑通再加,改一处验一处,别一次性动五个文件然后对着报错猜。

加字段是最小的改造单元,以商品加“规格”为例,链路是:数据库ALTER TABLE product ADD COLUMN spec VARCHAR(50),实体类加private String spec和 getter/setter,Mapper XML 的 insert 和 select 补上字段,JSP 表单加一个 input,controller 不用动。改完重启,新增一条带规格的商品,再查列表确认能显示,这条链路就闭环了。注意resultType自动映射靠的是列名和属性名一致,列名spec对应属性spec没问题,如果是product_spec就得配别名或开驼峰映射。

加模块的套路更固定:建表 → entity → mapper 接口 + XML → service 接口 + 实现 → controller → JSP。以“会员管理”为例,照着商品模块抄一遍结构,把表名、字段、业务校验换掉即可。真正要动脑的是权限:这个工程的登录拦截器只判断 session 里有没有用户,没有区分角色。要加管理员和收银员两种角色,得在用户表加role字段,拦截器里读角色判断 URL 前缀,比如/admin/**只允许管理员访问。拦截器写法如下:

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception { Object user = req.getSession().getAttribute("loginUser"); if (user == null) { resp.sendRedirect(req.getContextPath() + "/login"); return false; } // 角色校验:非管理员访问 /admin/ 开头路径直接拒绝 String uri = req.getRequestURI(); User u = (User) user; if (uri.contains("/admin/") && !"ADMIN".equals(u.getRole())) { resp.sendError(403, "无权限"); return false; } return true; } }

注册拦截器时排除登录页和静态资源,否则会死循环重定向。换前端的话,JSP 改成 Thymeleaf 或前后端分离,工作量主要在视图层,controller 返回 JSON 即可,service 和 mapper 基本不动——这也是分层带来的好处,改一层不牵连另一层。

验证改造是否成功,我一般用“三条链路法”:新增一条数据看能不能查出来、改一条数据看列表是否更新、删一条数据看关联表有没有残留。三条都过,再去看异常日志里有没有被吞掉的警告。从那以后我每次接手这类课设工程,都强制先把原始版本完整跑一遍再动手改,因为很多“改出来的 bug”其实是原工程就没跑通。希望这份拆解能帮你少走几个弯路,把这份 SSM 超市管理系统真正用起来。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/5 12:25:35

Text to SQL系统第三层实现:复杂查询意图识别与优化

1. 项目概述Text to SQL系统作为自然语言处理与数据库技术交叉领域的重要应用&#xff0c;正在深刻改变着非技术人员与数据库交互的方式。这个看似简单的"输入问题&#xff0c;输出SQL"功能背后&#xff0c;隐藏着复杂的实现逻辑和技术挑战。今天我们就来拆解这个系统…

作者头像 李华
网站建设 2026/11/5 11:36:48

PostgreSQL数据类型选择指南与性能优化实践

1. PostgreSQL数据类型概述PostgreSQL作为一款功能强大的开源关系型数据库&#xff0c;其数据类型系统之丰富远超大多数同类产品。我在实际项目中最常遇到的问题是&#xff1a;面对十几种数字类型、多种字符串类型以及各种特殊类型时&#xff0c;开发团队往往凭直觉选择&#x…

作者头像 李华
网站建设 2026/11/5 11:19:47

存储过程参数过多错误分析与解决方案

1. 问题现象与背景分析 最近在排查一个生产环境数据库问题时&#xff0c;遇到了"CDataBaseEngineSink::OnRequsetInsertCreateRecord 数据库异常&#xff1a;为过程或函数 GSP_GR_InsertCreateRecord 指定了过多的参数"的错误。这个错误发生在调用存储过程GSP_GR_Ins…

作者头像 李华
网站建设 2026/11/5 11:09:26

降低ai率的提示词5条拿来就用,改完朱雀AI率还高就免费降AI 1000字

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/5 10:30:39

AutoCAD零基础入门:坐标系、图层与打印出图习惯

1. 第一次打开AutoCAD时&#xff0c;真正该弄明白的是坐标系 很多人装完软件、双击图标之后&#xff0c;第一件事是去工具栏里找"直线"在哪、"圆"在哪&#xff0c;然后照着视频画一个方框&#xff0c;觉得自己入门了。等过两天要画一张带尺寸标注的简单零件…

作者头像 李华
网站建设 2026/11/5 10:29:05

临时邮箱在线生成与自建SMTP收信系统实践

主邮箱里塞满了各种"注册即送"的营销邮件&#xff0c;真正重要的账单通知反而被淹没——这个场景我遇到过不止一次。后来我开始用临时邮箱在线生成工具处理那些"只用一次"的注册场景&#xff1a;随便生成一个地址&#xff0c;收一封验证邮件&#xff0c;用…

作者头像 李华