资讯详情 SSM超市管理系统实战:从课设骨架到业务闭环的完整拆解
📅 2026/11/5 2:59:14
简介这份资源是面向高校计算机相关专业学生与Java入门开发者的SSM课程设计参考项目围绕超市管理场景解决商品、库存、销售等业务模块从建模到落地的完整实现问题适合正在做课程设计或需要熟悉Spring、Spring MVC与MyBatis整合流程的学习者。压缩包共792个文件约1.78MB以452个html页面、41个js脚本、16个css样式构成前端展示层48个java源文件与48个class编译文件对应后端控制器与实体另有28个xml配置、12个jsp视图及1个sql建库脚本完整覆盖从数据库到页面的各层结构。目前已有86人学习下载。项目按商品管理、库存出入库与盘点、销售订单与报表、会员积分及统计报表等模块组织读者可据此理解Spring事务管理保证数据一致性、MyBatis映射简化数据库交互的思路并参考Controller与实体类的划分方式快速搭建自己的课程设计骨架。1. 从一份“能跑起来的课设”说起SSM 超市管理系统到底解决了什么每年到了课程设计季后台总有人问同一个问题有没有一套结构完整、能直接跑、还能改的 SSM 项目答案往往让人失望——网上流传的很多“超市管理系统”要么只有几张截图要么代码里全是硬编码跑起来第一步就卡在数据库连接上。这套基于 Spring MVC MyBatis 的超市管理系统恰好是那种“拆开能看懂、跑起来能演示、改起来有抓手”的类型。它把商品管理、库存出入库、收银结算、会员与订单这几条超市业务主线串成了一个闭环前端用 JSP 配合少量 JavaScript 做页面渲染和表单校验后端按 Controller-Service-DAO 三层切分适合拿来做课设答辩也适合刚接触 SSM 整合的开发者拿来当练手骨架。下面我按“它是什么 → 怎么跑 → 怎么改”的顺序把这份源码拆给你看。2. 技术栈与分层架构一个请求在 SSM 里怎么走完2.1 为什么是 Spring MVC MyBatis 这套组合这套项目的技术选型非常“教科书”但对课设场景来说恰恰是最稳的。Spring 负责 Bean 的装配和事务管理Spring MVC 接管 Web 层的路由与参数绑定MyBatis 把 SQL 从 Java 代码里剥离出来放到 XML 映射文件前端则是 JSP JSTL 标签库 原生 JavaScript。四者各管一段边界清晰。我一般会把这种组合理解成一条流水线浏览器发请求 →DispatcherServlet统一入口 → 处理器映射找到对应 Controller 方法 → Service 层做业务判断和事务 → DAO 层通过 MyBatis 执行 SQL → 结果封装成ModelAndView或 JSON 返回 → JSP 渲染页面。理解这条链路后面看任何一段代码都不会迷路。选型上有几个现实考量值得说清楚。第一课设环境通常是 Tomcat MySQLSSM 对这两个的兼容性最好不需要额外容器。第二MyBatis 的 XML 映射让 SQL 可读性很强答辩时老师问“这个查询怎么写的”你直接翻 XML 就行比 JPA 的隐式生成 SQL 好解释。第三JSP 虽然老但服务端渲染对新手友好不用折腾前后端分离的跨域和打包。2.2 三层结构在目录里长什么样拿到源码先别急着跑先看目录。一个规范的 SSM 项目src/main/java下通常是这样切的src/main/java/com/supermarket/ ├── controller/ # 控制层接收请求、返回视图 │ ├── GoodsController.java │ ├── StockController.java │ ├── OrderController.java │ └── MemberController.java ├── service/ # 业务接口 │ └── impl/ # 业务实现事务在这里 ├── dao/ # MyBatis Mapper 接口 ├── entity/ # 实体类与表字段对应 └── util/ # 工具类如分页、日期转换 src/main/resources/ ├── mapper/ # MyBatis XML 映射文件 ├── spring/ # spring-context、spring-mvc 配置 ├── mybatis-config.xml └── jdbc.properties # 数据库连接参数 src/main/webapp/ ├── WEB-INF/ │ ├── views/ # JSP 页面 │ └── web.xml # DispatcherServlet 与过滤器配置 └── static/ # css、js、图片这个结构的关键在于controller只做参数接收和视图跳转service承担业务规则dao只负责数据访问。很多课设项目把业务逻辑全塞进 Controller导致后期改一个库存扣减规则要翻遍所有方法这套项目的分层相对干净二次开发时改动点很明确。2.3 一次商品查询的完整链路光看目录不够得跟一条真实请求。以“商品列表分页查询”为例从前端到数据库再回来代码是这样串起来的。Controller 层接收分页参数Controller RequestMapping(/goods) public class GoodsController { Autowired private GoodsService goodsService; // 商品列表带分页page 从 1 开始size 默认 10 RequestMapping(/list) public String list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, String keyword, Model model) { PageResultGoods result goodsService.queryByPage(page, size, keyword); model.addAttribute(pageResult, result); model.addAttribute(keyword, keyword); return goods/list; // 对应 /WEB-INF/views/goods/list.jsp } }这里RequestParam的defaultValue很关键它保证前端不传参时不会抛 400。keyword是可选参数用于商品名模糊搜索。返回的字符串goods/list会被InternalResourceViewResolver拼成/WEB-INF/views/goods/list.jsp。Service 层做分页计算和业务校验Service public class GoodsServiceImpl implements GoodsService { Autowired private GoodsMapper goodsMapper; Override public PageResultGoods queryByPage(int page, int size, String keyword) { // 页码兜底防止负数或 0 导致 SQL 偏移量错误 if (page 1) page 1; int offset (page - 1) * size; ListGoods rows goodsMapper.selectByPage(offset, size, keyword); int total goodsMapper.countByKeyword(keyword); return new PageResult(rows, total, page, size); } }offset的计算是分页最容易翻车的地方(page - 1) * size少减这个 1第一页就会跳过前size条数据。PageResult是个通用封装装当前页数据、总条数、页码和每页大小JSP 里直接拿它渲染分页条。DAO 接口和 XML 映射public interface GoodsMapper { ListGoods selectByPage(Param(offset) int offset, Param(size) int size, Param(keyword) String keyword); int countByKeyword(Param(keyword) String keyword); }select idselectByPage resultTypecom.supermarket.entity.Goods SELECT id, name, price, stock, category_id, create_time FROM goods where if testkeyword ! null and keyword ! name LIKE CONCAT(%, #{keyword}, %) /if /where ORDER BY id DESC LIMIT #{offset}, #{size} /selectwhere标签会自动处理AND前缀问题#{}是预编译占位符能防 SQL 注入——这一点答辩时经常被问到用${}拼keyword就是典型错误写法。LIMIT #{offset}, #{size}是 MySQL 的分页语法换成 Oracle 就得改成ROWNUM嵌套这也是数据库迁移时要注意的点。2.4 配置文件里藏着的运行前提SSM 项目跑不起来八成问题出在配置。jdbc.properties里是数据库连接四要素jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/supermarket?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password你的密码serverTimezone不写MySQL 8 驱动会报时区异常这是血泪经验。spring-mvc.xml里要配注解驱动、视图解析器和静态资源放行!-- 开启注解驱动RequestMapping 才生效 -- mvc:annotation-driven/ !-- 视图解析器逻辑视图名 - /WEB-INF/views/xxx.jsp -- bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /bean !-- 静态资源交给默认 Servlet 处理否则 css/js 会 404 -- mvc:default-servlet-handler/web.xml里则是DispatcherServlet的注册和CharacterEncodingFilter的编码过滤后者不配中文商品名存进数据库就是乱码。这几处配置串起来才构成一个能响应请求的最小闭环。3. 把项目跑起来环境、建库与启动排错3.1 环境清单与版本对齐跑这套项目之前先把版本对齐版本错配是新手最常见的翻车点。我一般建议按下面这套组合来组件建议版本说明JDK1.8SSM 老项目对高版本 JDK 兼容性差Tomcat8.5 / 9.0不要用 Tomcat 10包名从 javax 变 jakarta 会直接崩MySQL5.7 / 8.08.0 需注意驱动和时区Maven3.6用于依赖管理IDEIntelliJ IDEA / Eclipse需配置 Tomcat 集成Tomcat 10 这个坑必须单独说它把javax.servlet全部换成了jakarta.servlet而 SSM 项目里的web.xml和依赖还是老包名部署上去就是ClassNotFoundException。要么降 Tomcat要么改包名课设场景下前者省事得多。3.2 建库与初始化数据项目一般会带一个supermarket.sql脚本导入之前先建库# 登录 MySQL 后执行 CREATE DATABASE supermarket DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE supermarket; SOURCE /path/to/supermarket.sql;utf8mb4而不是utf8是因为后者在 MySQL 里只支持三字节存 emoji 或某些特殊字符会报错。导入后检查核心表是否齐全goods商品、stock_record出入库记录、orders订单、order_item订单明细、member会员、user系统用户。表之间的外键关系决定了后面删除商品时会不会被订单引用卡住这一点在改造章节还会提到。3.3 依赖与启动配置pom.xml里核心依赖是这几块spring-webmvc、spring-jdbc、mybatis、mybatis-spring、mysql-connector-java、jstl、servlet-apiscope 设为 provided因为 Tomcat 自带。mybatis-spring这个桥接包不能漏它负责把SqlSessionFactory交给 Spring 管理。启动方式有两种。用 IDEA 的话配置 Tomcat ServerDeployment 里加 Artifactwar explodedApplication context 设成/supermarket启动后访问http://localhost:8080/supermarket/。用 Maven 的话mvn clean package -DskipTests # 把 target 下的 war 丢进 Tomcat 的 webapps 目录启动 Tomcat-DskipTests是因为课设项目里的测试类经常依赖真实数据库打包时跑测试容易失败。war 包名决定了访问路径如果打出来是supermarket-1.0.war访问路径就是/supermarket-1.0想简化就在pom.xml的finalName里固定成supermarket。3.4 登录与核心业务流程验证跑起来后别急着点按业务顺序验证一遍才算真跑通。先用初始化脚本里的管理员账号登录进入商品管理新增一条商品然后去入库模块给这个商品加库存再去收银台下一单最后查订单列表和库存流水。这条链路走通说明 Controller、Service、DAO、事务、页面渲染全部正常。收银结算这块涉及事务Service 方法上一般会加TransactionalTransactional(rollbackFor Exception.class) public void checkout(OrderDTO orderDTO) { // 1. 生成订单主记录 Orders order buildOrder(orderDTO); orderMapper.insert(order); // 2. 逐条写入订单明细并扣减库存 for (OrderItem item : orderDTO.getItems()) { item.setOrderId(order.getId()); orderItemMapper.insert(item); int affected goodsMapper.decreaseStock(item.getGoodsId(), item.getQuantity()); if (affected 0) { // 库存不足抛异常触发回滚 throw new BusinessException(商品库存不足 item.getGoodsId()); } } }rollbackFor Exception.class是为了让受检异常也触发回滚默认只回滚RuntimeException。decreaseStock的 SQL 里带AND stock #{quantity}条件返回影响行数为 0 就说明库存不够抛异常让整单回滚避免出现“订单生成了但库存没扣”的脏数据。这个写法比先查再扣更安全能防并发超卖。4. 核心模块拆解商品、库存与收银三条主线4.1 商品管理CRUD 与分类联动商品模块是整套系统的地基其他模块都依赖goods表。它的 CRUD 看着简单但有两个细节值得注意。一是分类下拉框的联动新增商品时分类列表要从category表查出来放进Model二是删除商品时的引用检查如果商品已经产生过订单明细直接物理删除会破坏历史数据。我一般会把删除做成软删除给goods表加一个status字段1 上架、0 下架删除操作改成更新状态Override public boolean removeGoods(Integer id) { // 先检查是否有未完成订单引用有则拒绝 int refCount orderItemMapper.countByGoodsId(id); if (refCount 0) { throw new BusinessException(该商品已有订单记录不能删除请改为下架); } return goodsMapper.updateStatus(id, 0) 0; }这样既保住了历史订单的可追溯性又让商品从列表里消失。countByGoodsId这个查询在订单明细表上走一次索引成本很低。如果课设要求必须物理删除那至少要在删除前把关联的订单明细做归档否则订单详情页会显示“商品已不存在”。4.2 库存出入库流水记录与库存同步库存模块是这套系统里业务味最重的部分。入库和出库不能只改goods.stock一个字段还要往stock_record写一条流水记录操作类型、数量、操作人、时间。这样库存对不上时才有账可查不至于变成黑匣子。入库的核心逻辑Transactional(rollbackFor Exception.class) public void stockIn(Integer goodsId, int quantity, String operator) { if (quantity 0) { throw new BusinessException(入库数量必须大于 0); } // 1. 增加商品库存 int affected goodsMapper.increaseStock(goodsId, quantity); if (affected 0) { throw new BusinessException(商品不存在 goodsId); } // 2. 写入流水 StockRecord record new StockRecord(); record.setGoodsId(goodsId); record.setType(IN); record.setQuantity(quantity); record.setOperator(operator); record.setCreateTime(new Date()); stockRecordMapper.insert(record); }increaseStock的 SQL 是UPDATE goods SET stock stock #{quantity} WHERE id #{goodsId}用数据库层面的自增而不是先查后写避免并发下的丢失更新。出库同理只是把换成-并加库存充足判断。流水表的type字段用IN/OUT区分查询时按商品和时间排序就是一张完整的库存变动账。4.3 收银结算订单生成与库存扣减的原子性收银是三条主线里事务要求最高的。前面 3.4 已经贴过checkout的主体这里补充订单号生成和金额计算。订单号一般用“时间戳 随机数”或“日期 自增序列”课设场景下前者够用private String generateOrderNo() { // 格式yyyyMMddHHmmss 4 位随机数降低并发碰撞概率 String time new SimpleDateFormat(yyyyMMddHHmmss).format(new Date()); int random new Random().nextInt(9000) 1000; return time random; }金额计算要注意用BigDecimal而不是double浮点数在累加时会出现0.1 0.2 0.30000000000000004这种精度问题收银场景下这是不能接受的。订单总额在 Service 层算好再落库不要依赖前端传过来的金额否则被篡改就是资损。4.4 会员与订单查询多表关联的写法会员模块相对轻主要是会员注册、积分累计和按手机号查询。订单查询则涉及多表关联列表页要显示订单号、会员名、金额、状态详情页还要带出商品明细。列表查询用一条带LEFT JOIN的 SQL 就能搞定select idselectOrderPage resultTypecom.supermarket.entity.OrderVO SELECT o.id, o.order_no, o.total_amount, o.status, o.create_time, m.name AS memberName, m.phone AS memberPhone FROM orders o LEFT JOIN member m ON o.member_id m.id where if teststatus ! nullo.status #{status}/if if testkeyword ! null and keyword ! AND (o.order_no LIKE CONCAT(%, #{keyword}, %) OR m.phone LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY o.create_time DESC LIMIT #{offset}, #{size} /select用LEFT JOIN而不是INNER JOIN是因为散客订单可能没有会员INNER JOIN会把这类订单过滤掉。OrderVO是专门为页面展示建的视图对象比直接返回Orders实体多带了会员字段这种 VO 和 Entity 分离的做法在稍大的项目里很常见能避免实体类被页面需求污染。5. 避坑与常见问题排查那些让项目起不来的细节5.1 启动报 404页面就是不出来现象Tomcat 启动日志没报错但访问首页返回 404。原因通常是DispatcherServlet的映射路径和访问路径对不上或者spring-mvc.xml没被加载。解决检查web.xml里contextConfigLocation是否指向了正确的配置文件url-pattern是/还是*.do访问时路径要匹配。另外确认 war 的 context pathIDEA 里默认可能是项目名_war_exploded访问路径要跟着变。5.2 中文乱码商品名存进去变问号现象页面输入中文存库后变成???或乱码。原因有三层数据库字符集不是utf8mb4、JDBC URL 没带characterEncodingutf8、web.xml里没配CharacterEncodingFilter。解决三处一起改缺一不可。过滤器要放在所有 Servlet 之前forceEncoding设为true否则请求和响应只处理一边。5.3 事务不生效库存扣了订单没生成现象收银时抛了异常但库存已经被扣减。原因多半是Transactional加在了 Controller 上或者 Service 类没有被 Spring 扫描到又或者方法内部this调用导致代理失效。解决事务注解只加在 Service 实现类的 public 方法上确认spring-context.xml里context:component-scan的包路径覆盖到 service 包并且开启了tx:annotation-driven/。同类内部方法互调时用注入自身代理或拆到另一个 Bean 里。5.4 MyBatis 报 BindingExceptionMapper 找不到现象启动或调用时报Invalid bound statement (not found)。原因通常是mapperXML 没被扫描到或者 XML 里的namespace和接口全限定名不一致或者方法名和id对不上。解决检查mybatis-config.xml或 Spring 配置里mapperLocations的路径通配是否正确如classpath:mapper/*.xmlnamespace必须是接口的全限定名select的id必须和方法名一字不差。用 Maven 打包时还要确认 XML 被复制到了target/classes下pom.xml的resources要包含**/*.xml。5.5 静态资源 404页面没样式现象JSP 能打开但 css、js、图片全部 404。原因DispatcherServlet映射了/把所有请求都拦截了静态资源也被当成 Controller 请求处理。解决在spring-mvc.xml里加mvc:default-servlet-handler/或者用mvc:resources显式映射静态资源路径。前者交给容器默认 Servlet 处理配置最简单。6. 二次开发改造从课设骨架到能演示的业务系统拿到这套源码多数人的目标不是原样交差而是改成自己的题目或者加点亮点。改造要挑投入产出比高的地方下手我一般按“先稳后炫”的顺序来。第一刀砍在权限上。原项目如果只有一个管理员角色可以引入简单的角色表role、user_role在登录后把角色放进 Session用一个拦截器做菜单和接口的粗粒度控制。拦截器比 Spring Security 轻得多课设场景够用public class AuthInterceptor 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; } return true; } }在spring-mvc.xml里用mvc:interceptors注册mvc:exclude-mapping放行登录和静态资源。这样未登录访问任何业务页都会被弹回登录页演示时观感立刻不一样。第二刀是报表。库存和销售数据躺在表里没人看加一个按日期聚合的销售统计页用一条GROUP BY DATE(create_time)的 SQL 出每日销售额前端用 ECharts 画个折线图。数据量小的时候直接查库量大再考虑加汇总表。这一步的验证方法是手工在数据库里插几条不同日期的订单看图表数据是否对得上对不上就查 SQL 的日期函数和时区。第三刀是导出。订单列表加一个“导出 Excel”按钮用 Apache POI 把查询结果写成xlsx。注意流式写出和关闭顺序Workbook、OutputStream都要在finally里关否则文件被占用删不掉。导出接口的返回类型是void直接往response.getOutputStream()写不要走视图解析。改造过程中最容易忽略的是回归验证。每加一个功能把登录、商品新增、入库、收银、订单查询这条主链路重走一遍尤其是动了事务和库存逻辑之后。我吃过一次亏给收银加了优惠券抵扣结果把Transactional的传播行为改错了库存扣减在异常时没回滚测试数据里出现了负库存。从那以后我每次改完结算相关代码都强制走一遍“库存不足时下单”的异常用例确认整单回滚、流水干净。这套 SSM 超市系统的价值不在于代码多高级而在于它把一条完整业务链路摊开给你看改坏了能立刻从日志和流水里定位。希望这份拆解能帮你少走几个弯路把课设真正跑成自己的东西。本文还有配套的精品资源点击获取