SSM多门店库存系统:事务+乐观锁防超卖实战
2026/9/14 5:37:53 网站建设 项目流程

简介:本资源是一套完整的基于SSM框架的连锁便利店管理系统毕业设计项目,面向Java初学者及高校计算机相关专业学生,解决课程设计、期末大作业与毕业设计中企业级Web应用开发的实战需求。压缩包共1197个文件,涵盖256个HTML页面、226个CSS样式、204个JavaScript脚本、55个JSP视图、47个Java后端类、185个PNG与17个JPG图片资源,以及MySQL数据库脚本(.sql)、配置文件(.xml/.properties)和依赖JAR包等,完整呈现前后端分离式开发结构,包体大小为18.43MB。已有27人学习下载,适合需要理解SSM整合流程、掌握商品/库存/销售/会员/财务管理五大核心模块实现逻辑的学习者。资源包含可直接运行的工程结构、规范化的数据库设计、响应式前端界面及详细分层代码注释,有助于快速复现系统、调试业务逻辑并拓展二次开发。

1. 这不是又一个“增删改查”Demo:SSM连锁便利店系统真正解决的是多门店数据协同与实时库存穿透

你可能已经见过几十个标着“SSM毕业设计”的压缩包,点开后是千篇一律的用户登录+商品列表+订单管理——但这个基于SSM的连锁便利店管理系统,核心差异在于它默认按多门店架构建模,所有关键表(商品、库存、销售单)都强制绑定store_id字段,且库存操作全程走事务+乐观锁控制,避免总部调拨时A店扣减、B店未同步导致的超卖。它不只做单店模拟,而是把“跨店调货审批流”“区域库存汇总视图”“会员跨店积分合并”作为一级功能模块实现。适合正在做Java课程设计、需要体现分层架构理解力和真实业务复杂度的学生;也适合刚入职的开发,用来拆解一个结构清晰、边界明确、有完整事务链路的中小型系统。项目里没有花哨的微服务或Spring Cloud,但Spring声明式事务配置、MyBatis动态SQL嵌套、EasyUI树形菜单权限控制等细节,足够支撑起一次扎实的代码复盘。


2. SSM三层解耦如何落地:从Spring容器装配到MyBatis多数据源映射

2.1 Spring配置的核心逻辑:为什么用XML而非注解驱动?

本项目采用传统XML方式配置Spring容器(applicationContext.xml),并非技术落后,而是为清晰暴露IoC容器的装配链条。关键配置点有三处:

  • 事务管理器绑定DataSource<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">显式指定dataSource引用,确保后续@Transactional注解能正确拦截DAO层方法;
  • Service层Bean扫描范围限定<context:component-scan base-package="com.store.service"/>严格隔离Service包路径,避免Controller或DAO类被误注入;
  • MyBatis SqlSessionFactory集成:通过<bean class="org.mybatis.spring.SqlSessionFactoryBean">dataSourceconfigLocation(指向mybatis-config.xml)桥接,使MyBatis能读取全局配置(如<setting name="logImpl" value="LOG4J"/>)。

提示:若改为注解驱动,需在启动类添加@EnableTransactionManagement并确保@Service类被@ComponentScan覆盖,否则事务失效——这是学生作业中最常踩的坑。

2.2 MyBatis动态SQL实现跨店库存查询

库存管理模块需支持“查看某商品在所有门店的实时库存”,这要求MyBatis生成带IN子查询的SQL。对应Mapper XML片段如下:

<!-- InventoryMapper.xml --> <select id="getInventoryByGoodsId" resultType="Inventory"> SELECT i.id, i.store_id, i.goods_id, i.quantity, i.min_threshold, s.store_name FROM inventory i LEFT JOIN store s ON i.store_id = s.id WHERE i.goods_id = #{goodsId} <if test="storeIds != null and storeIds.size() > 0"> AND i.store_id IN <foreach item="storeId" collection="storeIds" open="(" separator="," close=")"> #{storeId} </foreach> </if> ORDER BY i.quantity DESC </select>
参数说明:
  • #{goodsId}:预编译参数,防SQL注入;
  • <if>标签判断是否传入门店ID列表,支持“查全量”或“查指定门店”两种场景;
  • <foreach>动态拼接IN语句,collection="storeIds"指代传入的List<Long>参数名,item="storeId"为循环变量名。

该SQL执行后返回Inventory对象集合,每个对象含门店名称(通过LEFT JOIN关联store表),前端EasyUI DataGrid可直接渲染为“商品ID | 门店名 | 当前库存 | 安全阈值”表格。

2.3 Spring MVC请求流转:从URL路由到JSON响应

Controller层采用RESTful风格设计,以销售单创建为例:

// SaleOrderController.java @PostMapping("/api/saleorder") @ResponseBody public Result createSaleOrder(@RequestBody SaleOrderRequest request) { try { saleOrderService.createOrder(request); return Result.success("销售单创建成功"); } catch (InsufficientStockException e) { return Result.fail("库存不足:" + e.getMessage()); } }
关键配置与逻辑:
  • @PostMapping("/api/saleorder"):匹配POST/api/saleorder路径,接收JSON请求体;
  • @RequestBody:将JSON自动反序列化为SaleOrderRequest对象(需含@Valid校验注解);
  • @ResponseBody:返回值经MappingJackson2HttpMessageConverter转为JSON,无需View解析;
  • 异常捕获块区分业务异常(如库存不足)与系统异常,保证前端拿到结构化错误码。

注意:Result类是项目自定义的统一响应封装,含code(200/500)、msgdata三字段,避免前端重复解析不同接口的返回结构。


3. EasyUI界面与权限控制:如何用角色ID驱动菜单动态加载

3.1 基于角色的菜单树生成逻辑

系统未使用Shiro或Spring Security,而是通过简单角色ID映射实现菜单控制。核心逻辑在MenuService中:

// MenuService.java public List<Menu> getMenusByRoleId(Long roleId) { // 1. 查询角色-菜单关联表 List<Long> menuIds = menuRoleMapper.selectMenuIdsByRoleId(roleId); // 2. 根据menuIds查菜单详情,并按parent_id构建树 List<Menu> allMenus = menuMapper.selectByIds(menuIds); return buildMenuTree(allMenus); } private List<Menu> buildMenuTree(List<Menu> menus) { Map<Long, List<Menu>> childrenMap = new HashMap<>(); List<Menu> rootMenus = new ArrayList<>(); // 分组:key=parent_id, value=子菜单列表 for (Menu menu : menus) { if (menu.getParentId() == null || menu.getParentId() == 0L) { rootMenus.add(menu); } else { childrenMap.computeIfAbsent(menu.getParentId(), k -> new ArrayList<>()).add(menu); } } // 递归挂载子菜单 for (Menu root : rootMenus) { root.setChildren(childrenMap.getOrDefault(root.getId(), Collections.emptyList())); } return rootMenus; }
数据库表设计要点:
  • menu表含idnameurlparent_idiconsort_order字段;
  • menu_role关联表存role_idmenu_id,实现N:N关系;
  • sort_order控制同级菜单显示顺序,避免拖拽排序后前端混乱。

3.2 EasyUI Tree组件渲染与点击事件绑定

前端使用EasyUI Tree加载菜单,HTML结构精简:

<!-- index.jsp --> <ul id="menuTree"></ul> <script> $(function() { $('#menuTree').tree({ url: '${pageContext.request.contextPath}/menu/list', method: 'get', animate: true, onClick: function(node) { if (node.url && node.url !== '#') { // 动态加载Tab页 addTab(node.text, node.url); } } }); }); </script>
关键行为说明:
  • url属性触发AJAX请求,后端返回JSON格式菜单树(含idtextchildren字段);
  • onClick回调中判断node.url是否为空,空则视为父节点(如“商品管理”),不跳转;非空则调用addTab()打开新Tab;
  • addTab()函数内部使用$('#tabs').tabs('add', {...})动态添加EasyUI Tabs,避免页面刷新。

3.3 权限拦截:Controller层的轻量级校验

为防止越权访问,每个业务Controller方法添加@PreAuthorize注解(需启用@EnableGlobalMethodSecurity):

// GoodsController.java @PreAuthorize("@permissionService.hasPermission(authentication, 'goods:edit')") @PutMapping("/goods/{id}") public Result updateGoods(@PathVariable Long id, @RequestBody Goods goods) { goodsService.updateById(goods); return Result.success(); }
PermissionService实现逻辑:
@Service public class PermissionService { public boolean hasPermission(Authentication auth, String permissionCode) { // 1. 从Authentication获取当前用户角色 Collection<? extends GrantedAuthority> authorities = auth.getAuthorities(); // 2. 查询该角色拥有的权限码列表(缓存优化) Set<String> userPermissions = permissionMapper.selectByRoleIds( authorities.stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toSet()) ); return userPermissions.contains(permissionCode); } }

提示:permissionCode格式为模块:操作(如inventory:query),比单纯角色判断更细粒度,且支持同一角色对不同模块设置不同权限。


4. MySQL数据库设计与事务安全:库存扣减的双重保障机制

4.1 关键表结构与外键约束

系统采用MySQL 5.7+,核心表设计遵循第三范式,重点字段如下:

表名字段类型约束说明
storeidBIGINT PKAUTO_INCREMENT门店主键
store_nameVARCHAR(50)NOT NULL门店名称
region_codeCHAR(6)INDEX所属行政区划码(用于区域汇总)
inventoryidBIGINT PKAUTO_INCREMENT库存记录主键
store_idBIGINTFK →store.id所属门店
goods_idBIGINTFK →goods.id商品ID
quantityINTDEFAULT 0当前库存量
min_thresholdINTDEFAULT 10安全库存阈值
versionINTDEFAULT 0乐观锁版本号
外键设计意义:
  • inventory.store_id外键确保每条库存记录必属某个有效门店,删除门店时自动拒绝(ON DELETE RESTRICT);
  • inventory.goods_id外键保证商品下架后无法新增库存,但允许历史库存保留(ON DELETE NO ACTION)。

4.2 库存扣减的事务与并发控制

销售下单时需原子性完成“扣库存→生成订单→记录流水”,代码位于SaleOrderService

@Transactional(rollbackFor = Exception.class) public void createOrder(SaleOrderRequest request) { // 1. 检查库存(SELECT FOR UPDATE) Inventory inventory = inventoryMapper.selectByStoreAndGoods( request.getStoreId(), request.getGoodsId() ); if (inventory.getQuantity() < request.getQuantity()) { throw new InsufficientStockException("库存不足"); } // 2. 乐观锁更新库存 int updated = inventoryMapper.updateQuantityOptimistic( inventory.getId(), inventory.getQuantity() - request.getQuantity(), inventory.getVersion() ); if (updated != 1) { throw new ConcurrentUpdateException("库存已被其他操作修改,请重试"); } // 3. 创建销售单与明细 saleOrderMapper.insert(order); saleOrderDetailMapper.insertBatch(details); }
事务与并发双保险:
  • @Transactional保证整个方法内所有DB操作要么全成功,要么全回滚;
  • SELECT ... FOR UPDATE在检查库存时加行锁,阻止其他事务同时修改同一库存记录;
  • updateQuantityOptimistic执行UPDATE inventory SET quantity=?, version=version+1 WHERE id=? AND version=?,利用version字段实现乐观锁,避免ABA问题。

4.3 索引优化与慢查询预防

针对高频查询场景,建立复合索引:

-- 加速“某门店某商品库存查询” CREATE INDEX idx_inventory_store_goods ON inventory(store_id, goods_id); -- 加速“某商品在所有门店的库存汇总” CREATE INDEX idx_inventory_goods ON inventory(goods_id); -- 加速销售单按时间范围查询 CREATE INDEX idx_saleorder_create_time ON sale_order(create_time);
索引设计依据:
  • idx_inventory_store_goods覆盖查询条件WHERE store_id=? AND goods_id=?,避免全表扫描;
  • idx_inventory_goods支持后台报表“商品销量TOP10”(需JOIN销售明细表);
  • idx_saleorder_create_time使SELECT * FROM sale_order WHERE create_time BETWEEN ? AND ?能在毫秒级返回。

注意:inventory.quantity字段未建索引,因该字段频繁更新,索引维护成本高于查询收益;实际查询均通过store_id+goods_id定位单行,无需范围扫描。


5. 毕业设计答辩高频问题与代码级应答策略

5.1 “为什么选SSM而不是Spring Boot?”

答辩时切忌回答“因为老师要求”或“网上教程多”。应聚焦技术选型逻辑:

  • 可控性优先:SSM XML配置显式暴露Spring容器、事务管理器、MyBatis工厂等组件,便于理解IoC/AOP原理,符合课程设计“掌握基础框架机制”的教学目标;
  • 学习曲线平缓:Spring Boot自动配置虽快,但隐藏了DataSource初始化、SqlSessionFactory构建等关键步骤,初学者调试事务失效问题时难以定位;
  • 兼容性验证:项目需对接学校老旧Tomcat 7服务器(仅支持Servlet 3.0),而Spring Boot 2.x默认要求Servlet 3.1+,SSM可无缝部署。

实操验证:在pom.xml中将spring-webmvc版本锁定为4.3.29.RELEASE,即可确保与Tomcat 7.0.96完全兼容。

5.2 “库存扣减怎么保证不超卖?”

必须展示具体代码位置与执行路径,而非泛泛而谈“用了事务”:

  • 第一道防线InventoryMapper.selectByStoreAndGoods()方法执行SELECT ... FOR UPDATE,在数据库层面锁定目标行;
  • 第二道防线InventoryMapper.updateQuantityOptimistic()使用version字段校验,SQL中WHERE id=? AND version=?确保更新前未被修改;
  • 第三道防线:Service层@Transactional包裹,若updateQuantityOptimistic返回0(即未更新任何行),抛出ConcurrentUpdateException触发事务回滚。
可现场演示的验证步骤:
  1. 启动两个浏览器标签页,登录同一门店账号;
  2. 同时提交两笔相同商品、相同数量的销售单;
  3. 观察日志:第一个请求成功,第二个请求抛出ConcurrentUpdateException,前端显示“库存已被修改,请重试”。

5.3 “EasyUI和Vue哪个更适合毕业设计?”

直击评审关注点——工程规范性与可维护性

  • EasyUI优势:组件成熟稳定(Tree/Grid/Tabs十年无重大漏洞),所有交互逻辑集中于index.jsp,无构建工具链,答辩时可直接打开.jsp文件讲解DOM操作;
  • Vue风险点:需npm run build生成静态资源,若学生未提交dist目录或vue.config.js配置错误,演示环境必然崩溃;且Vue Router的懒加载、Pinia状态管理等概念超出课程要求深度。

技术延伸:若需升级,建议采用Vue3 + Vite,但必须同步提供nginx.conf反向代理配置(解决跨域)和Dockerfile(确保环境一致性),否则答辩时演示失败概率极高。

5.4 防SQL注入与XSS攻击的具体实现

安全措施必须对应到代码行,拒绝“用了PreparedStatement”这类模糊表述:

攻击类型防御位置具体实现验证方式
SQL注入MyBatis Mapper所有参数使用#{}语法(如WHERE id = #{id}),禁用${}拼接goodsName参数中输入' OR '1'='1,观察是否报错或返回全部商品
XSS反射Controller返回值Result类中msg字段经StringEscapeUtils.escapeHtml4()处理输入用户名为<script>alert(1)</script>,检查页面源码是否转义为&lt;script&gt;alert(1)&lt;/script&gt;
越权访问@PreAuthorize注解hasPermission()方法中校验authentication.getAuthorities()与数据库权限码匹配用普通员工Token调用/api/goods/delete,HTTP状态码应为403
关键代码位置:
  • SQL注入防护:GoodsMapper.xml中所有<select>/<update>标签内参数均为#{xxx}
  • XSS防护:Result.java构造函数中this.msg = StringEscapeUtils.escapeHtml4(msg);
  • 越权防护:GoodsController.java@PreAuthorize("@permissionService.hasPermission(authentication, 'goods:delete')")

提示:答辩时可打开Chrome开发者工具→Network标签,筛选/api/goods/list请求,查看Response Body中msg字段是否已转义,比口头解释更有说服力。

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

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

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

立即咨询