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 iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mybatis/*.xml/ property nametypeAliasesPackage valuecom.shop.entity/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.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-packagecom.shop.service/ bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/事务管理器绑定同一个数据源开启注解驱动后service 方法上加Transactional就能生效。注意这里扫描的是com.shop.service如果 service 实现类放在别的包下事务不会生效——这是后面避坑章节要展开的一个高频问题。!-- spring-mvc.xml视图和静态资源 -- context:component-scan base-packagecom.shop.controller/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix 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-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 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-mappingforceEncodingtrue保证请求和响应都用 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) { PageResultProduct 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 PageResultProduct page(int page, int size, String keyword) { int offset (page - 1) * size; ListProduct 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 对应两条 SQLselect idselectPage resultTypeProduct SELECT id, code, name, price, stock, supplier_id AS supplierId FROM product where if testkeyword ! null and keyword ! AND name LIKE CONCAT(%, #{keyword}, %) /if /where ORDER BY id DESC LIMIT #{offset}, #{size} /select select idcount resultTypeint SELECT COUNT(*) FROM product where if testkeyword ! null and keyword ! AND name LIKE CONCAT(%, #{keyword}, %) /if /where /selectwhere标签会自动处理开头的 ANDif做动态条件拼接#{}是预编译占位符能防 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 idincreaseStock UPDATE product SET stock stock #{num} WHERE id #{productId} /update update iddecreaseStock UPDATE product SET stock stock - #{num} WHERE id #{productId} AND stock #{num} /update出库的 SQL 带了AND stock #{num}库存不足时更新影响行数为 0service 层据此抛异常回滚。这种“把校验写进 SQL 条件里”的做法比先查库存再判断更可靠因为查和改之间没有间隙。代价是错误提示不够细只能告诉用户“库存不足或商品不存在”但对课设场景够用了。3.3 收银下单订单主从表的一次性写入下单是业务链路最长的一环校验库存、生成订单主表、写入订单明细、扣减库存四步必须原子完成。Transactional(rollbackFor Exception.class) public Long createOrder(Long userId, ListOrderItem 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 用 8Tomcat 用 8.5 或 9。版本错配是启动失败的头号原因先把这张表对一遍再动手。组件建议版本说明JDK1.8工程用了 lambda 和部分 8 的语法Maven3.6负责依赖下载和 war 打包Tomcat8.5 / 9.0不要用 10包名已变成 jakartaMySQL5.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 里塞 jarwar 打包时会漏。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.propertiesjdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/shop?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.password你的密码MySQL 8 的驱动类是com.mysql.cj.jdbc.Driver5.7 用com.mysql.jdbc.DriverserverTimezone不写会在启动时报时区错误这是血泪经验里最常见的一条。useSSLfalse只是本地开发图省事生产环境不要照抄。4.3 启动方式与验证路径用 IDEA 的话配一个 Tomcat ServerDeployment 里加 war explodedApplication 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、连接串没带characterEncodingutf8、web.xml 没配编码过滤器。解决建库时指定DEFAULT CHARSET utf8mb4连接串补上编码参数过滤器设forceEncodingtrue三处缺一不可。改完记得重启 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.ProductMapperXML 放resources/mybatis/下方法名和 id 逐字核对。改完执行mvn cleantarget 里的旧 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/setterMapper XML 的 insert 和 select 补上字段JSP 表单加一个 inputcontroller 不用动。改完重启新增一条带规格的商品再查列表确认能显示这条链路就闭环了。注意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 超市管理系统真正用起来。本文还有配套的精品资源点击获取