简介:本资源是一套完整的基于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">将dataSource与configLocation(指向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)、msg、data三字段,避免前端重复解析不同接口的返回结构。
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表含id、name、url、parent_id、icon、sort_order字段;menu_role关联表存role_id与menu_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格式菜单树(含id、text、children字段);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+,核心表设计遵循第三范式,重点字段如下:
| 表名 | 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|---|
store | id | BIGINT PK | AUTO_INCREMENT | 门店主键 |
store_name | VARCHAR(50) | NOT NULL | 门店名称 | |
region_code | CHAR(6) | INDEX | 所属行政区划码(用于区域汇总) | |
inventory | id | BIGINT PK | AUTO_INCREMENT | 库存记录主键 |
store_id | BIGINT | FK →store.id | 所属门店 | |
goods_id | BIGINT | FK →goods.id | 商品ID | |
quantity | INT | DEFAULT 0 | 当前库存量 | |
min_threshold | INT | DEFAULT 10 | 安全库存阈值 | |
version | INT | DEFAULT 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触发事务回滚。
可现场演示的验证步骤:
- 启动两个浏览器标签页,登录同一门店账号;
- 同时提交两笔相同商品、相同数量的销售单;
- 观察日志:第一个请求成功,第二个请求抛出
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>,检查页面源码是否转义为<script>alert(1)</script> |
| 越权访问 | @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字段是否已转义,比口头解释更有说服力。
本文还有配套的精品资源,点击获取