简介这是一份基于SSMSpring Spring MVC MyBatis框架的超市管理系统课程设计资源面向计算机相关专业学生及需要快速完成SSM整合项目的开发者。系统涵盖商品管理、库存管理、销售管理、会员管理及统计报表等核心模块后端以Spring管理依赖与事务、Spring MVC处理请求、MyBatis操作数据库前端包含JSP视图、HTML页面及配套CSS、JavaScript脚本。资源包共792个文件压缩后约1.78MB以452个HTML页面为主体辅以48个Java源码、48个class编译文件、28个XML配置、41个JS脚本、12个JSP视图及SQL数据库脚本结构完整便于直接导入运行。目前已有84人学习浏览。对于课程设计或SSM框架学习者而言这套可运行的项目提供了清晰的分层代码结构、后台管理界面和数据库设计脚本可直接参考复用也可按需扩展新功能模块。1. SSM超市管理系统课程设计一份能跑通、能讲清、能改动的SSM架构参考很多人在课程设计里拿到“超市管理系统”这个题目时第一反应是去开源社区找个SSM项目直接改。但真等下载解压问题就来了配置文件互相报错、jar包版本对不上、前端页面和后端接口各说各话运气差的连Tomcat启动都过不去。这套SSM超市管理系统就是为这个场景准备的它把Spring管理对象、Spring MVC分发请求、MyBatis处理数据库这三件事完整跑通模块覆盖管理员登录、商品与供应商管理、进货与销售订单代码按Controller-Service-Dao三层组织。适合两类人第一次整合SSM、需要一份能照抄参考的新手以及想把课程设计的系统架构讲给答辩老师听的在校生。我按“架构→搭建→实现→排错→验收”的顺序拆开讲你能看到每个配置文件为什么这么写、每个接口怎么调、踩过的坑在哪里。2. SSM系统架构与数据表设计三个框架各管哪一段表怎么建才不返工一个常见的误解是Spring MVC处理请求、MyBatis操作数据库Spring只是把Service注册成Bean。实际上Spring的IOC容器把Controller层、Service层、Dao层对象串在一起Spring MVC只在Web层做请求映射与参数绑定MyBatis负责把SQL执行结果映射成Java对象。先把这个边界划清楚后面配置文件的写法才不容易乱。2.1 从JSP到数据库的请求流转一次保存操作经过几个类以“新增商品”为例完整请求链路是浏览器提交表单 → DispatcherServlet前置控制器 → HandlerMapping找到对应的Controller方法 → Controller调用Service接口 → Service实现类调用Mapper接口 → MyBatis执行XML里的SQL → 结果逐层返回 → 视图解析器渲染JSP页面。整条链路里真正需要手写的只有三块Controller方法、Service接口加实现类、Mapper接口加XML文件。Controller层代码最典型的写法如下Controller RequestMapping(/product) public class ProductController { Autowired private ProductService productService; RequestMapping(/list) public String list(Model model, RequestParam(defaultValue 1) Integer pageNum) { PageResultProduct result productService.findByPage(pageNum, 5); model.addAttribute(result, result); return product/list; } }逻辑说明Controller在这里只做三件事接收请求参数、调用Service、把返回结果放进Model供JSP渲染。RequestParam(defaultValue 1)表示URL上没有带pageNum参数时默认按第1页处理避免直接报400错误。这里有个容易被忽略的约定/product/list对应的是pageNum一个参数如果你在JSP里用了分页组件它可能传的是page或者currentPage那么RequestParam里的名字就要跟着改否则前端怎么点翻页都没反应还以为是SQL写错了。我在第一次写分页时就翻过车前端传page2后端接口收pageNum结果页面永远停在第一页。Service层是业务规则的落点。同一套Mapper接口可能在Service里被组合出不同含义比如“查询商品列表”和“查询库存不足的商品列表”用的都是selectByCondition差别在传入的查询条件对象。新手最容易犯的错是把业务判断写进Controller比如在Controller里判断库存是否足够、抵扣余额后能不能下单。课程设计规模不大时看不出问题但答辩时老师问“业务规则放哪一层”答不上来就很尴尬。我的习惯是Controller里只出现参数接收、Service调用、返回值处理任何判断都下沉到Service。2.2 数据表设计商品、供应商、订单的最小子集超市管理系统的课程设计最少需要五张表分别对应管理员、供应商、商品、订单、订单明细。表结构设计得合理后面代码能省一半功夫设计得不好查询统计时就得写一堆冗长的子查询。表名关键字段说明t_userid, username, password, role管理员账号role区分超级管理员和普通操作员t_supplierid, name, contact, phone供应商信息给进货单和商品表关联用t_productid, name, price, stock, supplier_id, image商品表supplier_id外键关联供应商t_orderid, order_no, total_amount, create_time订单主表存整笔订单的总金额和下单时间t_order_itemid, order_id, product_id, quantity, price订单明细一个订单对应多行明细t_order和t_order_item必须分开这是很多课程设计项目里“看起来能跑、实际很乱”的分水岭。如果只在订单表里放一个product_names字段把多个商品名拼接成字符串塞进去查询某个商品的销量时就得做字符串拆分既低效又容易被扣分。拆成主表和明细表后统计销量只需要对t_order_item做SUM(quantity) GROUP BY product_id一句SQL就能解决。建表时还要注意两个细节。第一t_product.stock不要用int以外的类型别设计成varchar然后在前端转数字排序和加减库存都会出莫名问题。第二外键supplier_id要加索引课程设计数据量小看不出差距但后续做供应商联动查询时走索引和全表扫描的响应时间完全是两种体验。2.3 各层类名与调用关系命名对齐比你想的重要三层架构的命名建议直接约定死t_product表对应Product实体、ProductMapper接口、ProductMapper.xml、ProductService接口、ProductServiceImpl实现类、ProductController控制器。命名完全对齐后有两个实际收益一是Mapper接口和XML文件同名同目录MyBatis扫描时不用额外配置映射关系二是代码生成器在生成基础增删改查时不会出现“接口改了参数、XML里还是旧SQL”的错位。我见过一个项目Dao接口叫ProductDaoXML叫ProductMapper.xml配置里还忘了指定mapperLocations结果项目启动时找不到SQL定义排查半天才意识到是命名不一致导致的。命名对齐不只是好看更是在减少黑匣子式的低效排错。只要约定统一哪怕哪天XML文件丢了看一眼接口名字就知道该去哪找。3. 搭建SSM工程骨架依赖版本、三个XML配置文件与Mapper扫描的绑定关系SSM整合最耗时间的环节不是写业务代码而是把Spring、Spring MVC、MyBatis三个框架拼起来。这一章我把Maven依赖、web.xml、spring-mvc.xml、applicationContext.xml、MyBatis配置逐项拆开每个配置解决什么问题、写错位置会有什么后果都会说清楚。3.1 Maven依赖版本对齐是第一道关卡SSM是一个组合框架版本冲突是所有问题的根源。以Spring 4.3.x搭配MyBatis 3.4.x为例这是我做课程设计时最常用的一组兼容性好网上资料也多。Spring 5.x搭配mybatis-spring2.x需要Servlet 3.1以上版本支持Tomcat 7以下会直接启动报错课程设计没必要冒这个险。properties spring.version4.3.18.RELEASE/spring.version mybatis.version3.4.6/mybatis.version /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version1.3.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.47/version /dependency /dependencies逻辑说明spring-webmvc和spring-jdbc都依赖spring-core、spring-context等基础模块Maven会自动传递不需要逐个手动添加。mybatis-spring1.3.2是为Spring 4.3服务的换到2.x就需要Spring 5这是最常见的依赖冲突来源。这里有一个容易反复踩的坑mysql-connector-java版本必须和本机MySQL版本匹配。如果你本机装的是MySQL 8.x还在用com.mysql.jdbc.Driver这个驱动类连接池会报ClassNotFoundException。正确做法是升级驱动到8.0.x并把驱动类改成com.mysql.cj.jdbc.Driver。我一般在jdbc.properties里同时保留两个注释行环境切到MySQL 8时改一行驱动名加一行serverTimezoneAsia/Shanghai就行。3.2 web.xml里的DispatcherServlet启动顺序与映射规则web.xml是Web应用的入口里面需要配置两个关键东西DispatcherServlet和CharacterEncodingFilter。DispatcherServlet负责接管所有请求CharacterEncodingFilter解决POST乱码。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 servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mapping逻辑说明forceEncodingtrue表示不管请求本身带的编码是什么都强制使用UTF-8少了这个参数POST提交的中文大概率乱码。load-on-startup必须写成1否则Tomcat启动时不会初始化Spring容器第一次访问页面才初始化报错时机难判断。url-pattern写/表示所有请求都交给DispatcherServlet处理这样Controller里的RequestMapping才能生效。但这也意味着JSP、CSS、JS等静态资源也会被拦截所以spring-mvc.xml里必须配置静态资源放行否则页面样式全丢第5章我会专门展开这个坑。3.3 applicationContext.xml与spring-mvc.xml父子容器的分工SSM配置文件的核心是父子容器概念。applicationContext.xml是父容器管理Service、Dao、数据源、事务spring-mvc.xml是子容器只管理Controller和视图解析器。子容器可以引用父容器的Bean父容器不能反向引用子容器。!-- applicationContext.xml -- context:component-scan base-packagecom.example.service context:exclude-filter typeannotation expressionorg.springframework.stereotype.Controller/ /context:component-scan !-- spring-mvc.xml -- context:component-scan base-packagecom.example.controller/逻辑说明父容器的扫描要排除Controller子容器的扫描只扫Controller包。这种分工是避免“一个Bean被创建两次”的关键。如果不做排除Spring容器里会出现两个同名ServiceAutowired注入时可能因为歧义而报错或者行为不一致非常难排查。这里还要加一个关键配置在spring-mvc.xml里开启动态资源和注解驱动。mvc:annotation-driven/ mvc:default-servlet-handler/annotation-driven负责注册RequestMappingHandlerMapping和RequestMappingHandlerAdapter没有它Controller上的RequestMapping完全失效。default-servlet-handler把静态资源请求交回Tomcat默认Servlet处理避免CSS、JS被DispatcherServlet拦掉。3.4 MyBatis配置Mapper扫描与XML路径的绑定MyBatis整合Spring的关键是MapperScannerConfigurer它扫描指定包下的Mapper接口并为每个接口生成代理对象。这个配置写错位置后果是启动不报错、运行时报空指针。bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.example.dao/ /bean逻辑说明basePackage填的是Mapper接口所在的包名MyBatis会扫描该包下所有接口把接口和同名XML文件绑定。这里隐含一个约定ProductMapper.java必须和ProductMapper.xml同名且在同一包路径下。如果XML放在resources目录但没有建包目录或者名字差一个字母MyBatis会报Invalid bound statement (not found)。这个报错的意思是接口方法找到了但SQL语句没有找到对应实现。在同名同目录的约定下MyBatis还会自动应用驼峰映射。数据库字段create_time对应实体属性createTime只要在MyBatis配置里开启settings setting namemapUnderscoreToCamelCase valuetrue/ /settings开启之后查询结果就能自动把下划线字段名转成驼峰属性名省掉大量resultMap。我不太建议在课程设计里手写resultMap映射所有字段降级成一个resultType加上驼峰映射代码量少一半出错概率也低很多。4. 核心功能实现登录拦截、商品管理与前端交互框架搭好之后剩下的就是业务功能怎么写了。登录拦截、商品增删改查、分页模糊查询这三个功能基本覆盖了超市管理系统的主线写完它们订单和供应商模块的套路完全一样。4.1 登录拦截器别把静态资源也拦了登录状态校验是课程设计的必考点最常见方案是写一个HandlerInterceptor在请求进入Controller之前检查Session。如果没登录直接重定向到登录页。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user request.getSession().getAttribute(loginUser); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return false; } return true; } }逻辑说明preHandle返回false时请求在拦截器处终止不会到达Controller。loginUser是登录成功时往Session里放的对象。判断为空就重定向到登录页。拦截器注册在spring-mvc.xml里mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/css/**/ mvc:exclude-mapping path/js/**/ mvc:exclude-mapping path/images/**/ /mvc:interceptor /mvc:interceptors逻辑说明mapping path/**表示拦截所有请求exclude-mapping放行登录接口和静态资源目录。/login是登录Controller的URL如果不放行就会出现“还没登录连登录页都进不去”的逻辑死循环。这里的常见误用是只拦截*.action或/pages/**漏掉了JSP直接访问的场景。实际浏览器里直接输/product/list.jsp也能绕过拦截器因为JSP请求默认不走Spring MVC。所以要么把JSP页面全部放在WEB-INF/views下并只通过Controller跳转要么拦截路径写成/**再显式放行登录和静态资源。我一般选择后者改动最小且不容易漏。4.2 商品管理增删改查Controller传参的细节商品保存是典型的表单提交场景。表单里有商品名、价格、库存、供应商下拉框、图片路径等字段Spring MVC可以直接把同名表单字段封装进Product实体。PostMapping(/save) public String save(Product product, RequestParam(supplierId) Integer supplierId) { productService.save(product); return redirect:/product/list; }逻辑说明Spring MVC在做参数绑定时会自动调用Product的setter方法把请求参数按同名规则填入对象属性不需要手写request.getParameter。RequestParam(supplierId)单独接收下拉框选中的供应商编号因为表单里这个参数名和Product的属性名不一定一致。redirect:前缀是做“保存后重定向”的关键。如果直接返回product/list字符串等于转发到列表JSP但浏览器地址栏还是/product/save。刷新页面时表单会被重复提交订单和商品会出现两条重复数据。用redirect:/product/list让浏览器重新发起GET请求刷新页面就没有副作用了。有一个细节需要提醒Product里如果还有图片上传字段multipart/form-data类型需要额外配置MultipartResolverbean idmultipartResolver classorg.springframework.web.multipart.commons.CommonsMultipartResolver property namemaxUploadSize value10485760/ /beanmaxUploadSize单位是字节10485760即10MB超过这个大小会抛异常。不配置这个BeanRequestParam(file) MultipartFile会直接报空指针或400错误。4.3 分页与模糊查询参数封装与SQL拼接的简化方案分页和模糊查询是系统展示页最常见的组合需求。前端传pageNum、pageSize和keyword三个参数后端计算偏移量在MyBatis里动态拼接SQL条件。先看Controller和ServiceRequestMapping(/list) public String list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 5) Integer pageSize, RequestParam(required false) String keyword) { int offset (pageNum - 1) * pageSize; PageResultProduct result productService.findByPage(offset, pageSize, keyword); model.addAttribute(result, result); return product/list; }这里有一个容易搞错的换算关系数据库LIMIT的偏移量从0开始前端页码从1开始。所以offset (pageNum - 1) * pageSize。如果直接把pageNum传给LIMIT第一页正常第二页会跳过一条本该显示的数据属于典型的“差一”错误。再来看XML里的动态SQLselect idfindByPage resultTypecom.example.entity.Product SELECT * FROM t_product where if testkeyword ! null and keyword ! AND name LIKE CONCAT(%, #{keyword}, %) /if /where ORDER BY id DESC LIMIT #{offset}, #{size} /select逻辑说明where标签会自动去掉条件开头多余的AND这样即使keyword为空生成的SQL是SELECT * FROM t_product ORDER BY id DESC而不是SELECT * FROM t_product WHERE ORDER BY。if标签判断传入的查询条件名字为空时忽略该条件。CONCAT(%, #{keyword}, %)是MySQL安全的字符串拼接写法同样条件下用%${keyword}%会有SQL注入风险不建议。使用#{}和${}的边界也在这里#{}是预编译占位符MySQL会转义传入值${}是直接拼接字符串值里出现单引号就可能导致SQL语法错误甚至注入。课程设计里如果图省事全部用${}答辩时被问到SQL注入问题会很被动。我的原则是所有条件值都用#{}只有表名、排序字段等结构性的东西才考虑${}并且要做白名单校验。5. SSM超市管理系统避坑与排查五个翻车现场必看框架整合类项目的高频错误高度集中我把最常见的五类问题按“现象→原因→解决”整理出来。每一类都付出了实际调试时间照着排查省一半时间。5.1 启动后访问404DispatcherServlet把静态资源吞了现象项目能在Tomcat启动数据库连接也正常但访问登录页时CSS样式全丢直接访问Controller的URL能出页面访问/js/jquery.js却404。原因DispatcherServlet配置时url-pattern用了/把静态资源请求也拦截了。Spring MVC没找到对应的Controller方法返回404。解决在spring-mvc.xml里加一行mvc:default-servlet-handler/。加上以后Spring MVC会把没有匹配到Controller的请求交回Tomcat默认的Servlet处理CSS、JS、图片正常返回。这个配置丢失的时候后台不报错第一次遇到基本靠猜。5.2 查询结果返回nullMapper接口有方法但XML没绑上现象代码运行没异常Service调Mapper接口时也能进入方法但返回的ListProduct里元素是null或者直接报Invalid bound statement (not found): com.example.dao.ProductMapper.findByPage。原因接口方法名和XML的select的id不一致或者XML文件位置不对。MyBatis通过接口全限定名去匹配同名XML如果ProductMapper.xml放到了resources/com/example/dao/以外的地方或者文件名差一个字母就找不到SQL。解决先检查target目录有没有编译后的XML文件。IDEA里常见情况是XML忘在src/main/java目录下但构建时没有把XML复制进classes目录。我在pom.xml里一般会加一段构建配置确保XML和properties文件随编译输出build resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource resource directorysrc/main/resources/directory /resource /resources /build这段配置的含义是src/main/java和src/main/resources下的XML文件都参与构建。不少课程设计项目把Mapper XML放在Java包目录下不加这段配置部署后就是上面那个报错。5.3 中文乱码POST、GET、数据库三个环节逐个排查现象新增供应商时输入中文名保存后页面显示???但列表页其他数据正常。有时同一个操作POST是乱码GET却没乱码。原因乱码可能是三个环节中的任意一个造成的。POST请求时web.xml没有配CharacterEncodingFilter数据库连接URL没有指定characterEncodingutf8或者JSP的pageEncoding和表字段的字符集不一致。解决按顺序排查。第一步确认web.xml里的编码过滤器配了forceEncodingtrue只设encodingUTF-8不设force时已解析的请求体不会重新编码。第二步确认jdbc.properties里URL带了useUnicodetruecharacterEncodingutf8。第三步用Navicat或命令看表结构确认表字段是utf8或utf8mb4不是latin1。我自己习惯把CharacterEncodingFilter的url-pattern写成/*而不是/区别在于/*能过滤到包括JSP在内的所有请求避免JSP响应里的中文也踩坑。5.4 新增商品后回显的id是null现象插入商品成功后代码里调用product.getId()拿到的是null后续要用这个id插入订单明细时直接空指针。原因数据库主键使用了自增但MyBatis不知道插入成功后要回填自增主键。如果XML里没有配置useGeneratedKeysMyBatis只执行插入不返回任何自增值。解决insert标签上显式声明insert idinsertProduct useGeneratedKeystrue keyPropertyid INSERT INTO t_product(name, price, stock, supplier_id) VALUES(#{name}, #{price}, #{stock}, #{supplierId}) /insertuseGeneratedKeystrue让JDBC的getGeneratedKeys()生效MySQL会把自增id回写到Product实体的id属性上。keyProperty指定的是Java实体属性名不是数据库列名。如果这里写成keyColumnid也能用但keyColumn的语义不同多表关联时才建议使用。5.5 前端表格渲染出undefinedJSON字段名对不上现象浏览器Network里能看到后端返回JSON结构也正常但页面表格里全是undefined控制台报Cannot read properties of undefined。原因前端取的字段名和后端实体属性名不一致。常见于数据库字段是create_time实体属性是createTime后端返回的JSON key是createTime前端却写了create_time。或者是后端返回的是ListMapString, ObjectMap的key大小写和前端不一致。解决这是纯前端问题先去Network看返回JSON的实际key名再改前端取值或调整后端返回结构。我的习惯是列表页面不做直接实体输出Controller返回一个封装好的Map或DTO字段名显式指定和前端约定好。比如商品列表需要展示供应商名称Product实体里没有这个字段就在DTO里加一个supplierName字段避免前端硬拼两个接口的数据。如果上面五个问题都排查完还没解决最后再做三件事第一看Tomcat catalina日志里最近一次完整的异常堆栈定位到具体行号第二把target目录删掉重新mvn clean package排除旧的class和XML文件残留第三检查IDEA的Maven配置是否指向本机仓库换过JDK或仓库路径后经常出现依赖加载不全。这三步走完九成以上的环境问题都能浮出水面。6. 从“能跑”到“能讲”验收前的验证清单与两个改动建议课程设计答辩时老师看得最多的不是代码量而是演示流程是否完整。我建议在提交前走一遍全流程自测每一步都有明确预期整个过程约八分钟。6.1 八分钟自测路径步骤操作预期结果1启动Tomcat访问登录页页面样式正常无4042输入管理员账号登录跳转到首页Session写入登录用户3新增一个商品并上传图片列表第一条出现该商品中文显示正常4编辑该商品价格列表价格即时更新5按关键字模糊搜索只返回匹配商品分页可翻到第二页6为商品新增一笔进货订单商品库存数量增加7退出登录跳回登录页保留登录前页面8未登录直接访问商品列表URL被拦截器重定向到登录页第8步是拦检查拦磏器的关键验证。很多项目登录后一切正常但退出后直接在浏览器输入/product/list还能访问说明拦截器漏配了路径这是答辩现场最容易被指出来的问题。6.2 两个低成本改动如果时间充裕我建议做两个低成本增强它们的工作量都不大但能让系统看起来更有“业务完整度”。第一个是供应商联动的Ajax查询。在订单页面选供应商后通过getJSON请求后端/supplier/{id}/info把联系人、电话显示在旁边输入框。这个改动只需要在Controller加一个ResponseBody方法前端加十来行jQuery代码。它证明你理解了前后端数据交互而不只是会return jsp。第二个是库存预警。给t_product加一个warning_stock字段商品列表Page渲染时判断stock warning_stock行背景标成浅红色。SQL里不需要任何改动Mapper查询时把两个字段都查出即可前端JSP里加两个c:if标签就完成。这个功能是超市管理的典型业务场景答辩时能自然讲出“低库存会阻碍销售”这类业务价值。我第一次做课程设计时为了赶进度跳过了拦截器配置结果答辩现场演示时直接从列表页点进去被老师点了一句“你没登录怎么进来的”。从那以后我每次做完系统都强制自己从登录页重新走一遍全流程验证完再交付。那个教训让我养成一个习惯任何SSM项目改完代码第一件事不是看功能而是先验证登录跳转和权限拦截。这份资源里同样包含完整的数据库脚本和配置注释希望这篇拆解能帮你少踩几个坑动手跑一遍比干看文章有用得多。希望帮到你。本文还有配套的精品资源点击获取