ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SSM电商实战:从分层架构到订单事务与库存扣减

SSM电商实战:从分层架构到订单事务与库存扣减 简介SSM项目鲜花销售管理系统.zip 是一套基于 SpringSpringMVCMyBatis 框架的完整 Java Web 实战项目适合正在学习 SSM 整合、MySQL 数据库及前端 LayUI 的中高级开发者。资源共收录 637 个文件压缩后约 22.68MB包含 105 个 Java 源文件、80 个 JSP 页面、98 个 JavaScript 脚本、63 个 CSS 样式以及 SQL 建表脚本与 IDE 配置文件完整覆盖后端业务、前端交互与数据初始化。系统围绕商品管理、订单管理、用户管理和库存报表等模块展开可帮助读者理解 SpringMVC 控制器调度、MyBatis 持久化映射和 LayUI 管理界面的联动实现。资源包目录按 Eclipse/IDEA 工程结构组织附带数据库脚本与配置文件便于直接导入运行并二次开发。目前已有 3121 人学习下载是演练企业级 SSM 开发流程、积累项目经验的实用参考。1. 一套能直接跑起来的 SSM 电商范本拆开它你就懂了 Java Web 的完整套路如果你已经学完了 SSM 框架的基础概念却始终没找到一个「业务完整、代码不过度封装、能照着敲一遍」的练手项目那么这套鲜花销售管理系统值得你花一个下午拆一遍。它基于 Spring SpringMVC MyBatis 组合后端用 MySQL 存储商品、订单和客户数据前端用 LayUI 搭后台管理界面覆盖了商品管理、订单流转、库存扣减、会员登录这些电商系统里最典型的功能。它不炫技但每一层都踩在 Java Web 开发的常规路线上Controller 收请求、Service 做业务、Mapper 写 SQL全部按标准分层适合用来理解 SSM 三兄弟各自该干什么。这篇博文我会从数据库表结构讲到部署运行再把导入 ZIP 源码时最容易翻车的几个坑一并说清楚。你拿到的重点不是项目本身而是这套项目背后可复用的 SSM 工程组织方式。2. SSM 三层架构下的角色划分先弄清 Spring、SpringMVC、MyBatis 在这个项目里各管什么2.1 为什么偏偏是 SSM 而不是 Spring Boot在 Spring Boot 普及之前SSM 是最主流的企业级组合现在大量存量系统仍然跑在这套架构上。鲜花销售管理系统选择 SSM原因很实际Spring 负责对象的创建和依赖注入也就是常说的 IoC 容器SpringMVC 负责把浏览器发来的 HTTP 请求路由到对应的 Java 方法MyBatis 负责把 SQL 语句和 Java 方法绑定简化 JDBC 操作。三者各管一层边界清楚。对比 Spring BootSSM 需要显式写 XML 配置或者 Java Config 来完成组件扫描、视图解析器、数据库连接池的装配。这个「麻烦」反而成了学习优势你会清楚每个注解、每个 XML 标签最终影响了哪个环节。如果直接上手 Spring Boot很多配置被自动约定隐藏了出了问题反而不知道去哪排查。所以对于想深入了解 Java Web 底层协作机制的人来说SSM 项目的价值并不过时。2.2 典型请求在项目里的完整路径打开这个项目的源码建议你带着一个具体的请求去读比如「管理员在界面上点击添加商品」。这个过程在代码里是这样流转的浏览器发送 POST 请求到/flower/admin/product/save附带表单参数。SpringMVC 的DispatcherServlet拦截到这个 URL通过HandlerMapping找到对应的ProductController方法。Controller 调用ProductService的saveProduct方法Service 里做了参数校验和业务规则判断。Service 调用ProductMapper的insert方法MyBatis 把这个 Java 方法映射到 XML 或注解里的 SQL 语句。SQL 执行完成后返回值逐层返回到 ControllerController 再返回一个视图名或 JSON 字符串。SpringMVC 通过视图解析器拼装出 JSP 页面或者直接把 JSON 交给前端 LayUI 的表格组件渲染。这个链路是 SSM 项目的通用骨架。你不需要死记每一步但需要能在源码里找到对应的类和配置。下面用表格梳理几类核心文件的位置方便你对照源码定位。层次典型包名代表类 / 文件核心作用控制层com.xxx.controllerUserController、ProductController接收请求、参数绑定、返回视图或 JSON业务层com.xxx.serviceUserService、OrderService业务逻辑、事务管理、状态流转持久层com.xxx.mapperUserMapper、OrderMapper定义数据库操作方法配合 XML 写 SQL实体层com.xxx.entityUser、Product、Order与数据库表对应的 Java 对象前端资源webapp/staticlayui.js、bootstrap.min.css页面样式和交互组件2.3 Spring 配置文件里最值得看的三个 Bean项目里的applicationContext.xml或spring-mvc.xml是 SSM 项目的命脉。我建议你打开配置文件后重点找这三个配置!-- 开启注解扫描只扫 controller 之外的包 -- context:component-scan base-packagecom.xxx context:exclude-filter typeannotation expressionorg.springframework.stereotype.Controller/ /context:component-scan !-- 配置 MyBatis 的 SqlSessionFactory注入数据源和 mapper 映射文件 -- bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean !-- 声明式事务管理器在 Service 方法上通过 Transactional 控制事务 -- bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean第一个component-scan告诉 Spring 去哪里找带有Service、Repository注解的类同时过滤掉Controller因为控制层要交给 SpringMVC 的子容器管理。第二个sqlSessionFactory中的mapperLocations属性非常关键它决定了 MyBatis 的 XML 映射文件存放目录。如果路径不对项目启动时就会报Invalid bound statement (not found)错误。第三个事务管理器让写操作进入同一个事务比如创建订单时需要同时更新订单表和扣减库存任何一个步骤失败都会整体回滚避免出现只下了单但库存没扣的脏数据。这些配置虽然不是业务代码但如果你要把系统部署到新环境它们才是决定能否启动的基石。换个数据库、换个端口或者想给 SQL 打印日志都要在这里动手脚。3. 本地复现把 ZIP 源码变成可运行的 Web 项目3.1 开发环境选型Eclipse EE 还是 IDEA这个项目支持 Eclipse EE 或 IDEA但我个人更推荐 IDEA原因有两点IDEA 对 Maven 项目的导入识别更干净不会出现 Eclipse 里常见的「项目导入了但依赖缺失」的假报错另外 IDEA 的插件生态对 Spring 和 MyBatis 的跳转支持更好你可以直接点击方法名跳转到 XML 里的 SQL 语句。如果你习惯 Eclipse也完全没问题只要注意导入的是pom.xml或.project文件而不是把整个 ZIP 当成文件夹拷进工作区。在开始之前先确认以下工具已经装好JDK 8 或 JDK 11SSM 项目多数基于 JDK 8 编写高版本可能编译不兼容。Maven 3.6项目里的依赖通过 Maven 管理不需要手动下载 JAR 包。MySQL 5.7 或 MySQL 8.0需要本地能访问localhost:3306。Tomcat 8.5 或 Tomcat 9用于部署war包。3.2 数据库初始化执行 SQL 脚本ZIP 解压后在根目录或sql目录下能找到flower.sql或flower_system.sql。打开这个文件你会看到CREATE DATABASE和多个表的建表语句。最稳妥的执行方式是在命令行里登录 MySQL然后指定字符集导入mysql -uroot -p -e CREATE DATABASE IF NOT EXISTS flower_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; mysql -uroot -p flower_db flower.sqlutf8mb4比utf8多支持了表情符号和更多生僻字对鲜花名里的特殊字符兼容更好。执行后可以用SHOW TABLES验证正常的系统至少会有下面这些表SHOW TABLES FROM flower_db; -- 预期结果包含 admin, user, category, product, orders, order_item, cart, address其中orders表和order_item表是拆分的一对多关系订单主表存总额和状态子表存每个商品的购买数量和快照价格。这种设计很经典订单生成后即使商品改了价格订单里保存的依然是下单那一刻的价格。导入后如果看到ERROR 1366或中文乱码多半是客户端字符集没有切换成utf8mb4可以在连接命令里加上--default-character-setutf8mb4再执行一次。3.3 修改数据库连接配置数据库建好后接下来要找到项目的 JDBC 配置文件。常见的名字有jdbc.properties、db.properties或者直接写在spring-mybatis.xml里。打开后你会看到类似这样的内容jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/flower_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456请注意serverTimezone这个参数在 MySQL 8.0 下如果不写连接时大概率会报时区相关的异常。另外 MySQL 8.0 的驱动类名应该改成com.mysql.cj.jdbc.Driver而 5.7 用com.mysql.jdbc.Driver即可。如果项目用的驱动版本太老连不上 8.0 数据库时别忘了检查有没有把mysql-connector-java的 Maven 依赖升到 8.0 以上的版本。3.4 启动 Tomcat 并验证登录IDEA 里配置好 Tomcat 后直接 Run 启动。控制台出现Spring Context initialized和Server startup in ... milliseconds说明加载成功。打开浏览器访问http://localhost:8080/flower_system/如果看到跳转到登录页说明静态资源和 SpringMVC 的映射都正常。管理员账号密码一般在 SQL 脚本里有INSERT INTO admin语句默认可能是admin / admin或者admin / 123456。登录失败时优先检查admin表的password字段是否加密如果存的是密文那密码也应该是对应明文加密后的结果。这个过程中最常见的错误是启动时报404但页面资源处于不加载状态。我一般会先打开浏览器的开发者工具看 Network 面板如果所有 JS、CSS 都返回 200只有接口报 404问题出在 SpringMVC 的DispatcherServlet拦截路径配置如果静态资源也是 404那就是web.xml里过滤器拦截了.js.css文件或者没有启用mvc:default-servlet-handler/。排查顺序是资源路径 → 控制器映射 → 视图解析器前缀后缀。4. 核心业务拆解商品维护、下单事务与库存防超卖实现4.1 商品管理里的文件上传与 LayUI 表格渲染商品管理页面用的是 LayUI 内置的table模块后端返回 JSON 数据格式必须符合 LayUI 的约定。比如分页接口返回{ code: 0, msg: , count: 12, data: [ {id: 1, name: 红玫瑰, price: 99.9, stock: 200}, {id: 2, name: 香槟玫瑰, price: 129.0, stock: 150} ] }code必须是 0 才表示成功count代表总条数data是当前页的数据列表。后端对应的分页查询逻辑通常写在ProductController里Controller RequestMapping(/flower/product) public class ProductController { Autowired private ProductService productService; RequestMapping(/list) ResponseBody public LayuiResult list(RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int limit) { PageResultProduct pageResult productService.getProductPage(page, limit); return LayuiResult.success(pageResult.getTotal(), pageResult.getList()); } }page和limit是 LayUI 表格默认传过来的分页参数一个代表当前页码一个代表每页条数。后端拿到这两个参数后MyBatis 的 SQL 需要使用LIMIT #{offset}, #{size}来实现分页。注意offset (page - 1) * limit如果在 SQL 里直接用LIMIT #{page}, #{limit}得到的结果会从第 1 条开始而不是第 1 页的正确数据。这个坑新手经常踩表现为第二页数据从中间位置开始。商品图片上传也是商品管理里绕不开的功能。这个项目的前端用的是 LayUIupload模块后端可以单独写一个FileControllerPostMapping(/upload) ResponseBody public MapString, Object upload(RequestParam(file) MultipartFile file) { String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String newName UUID.randomUUID().toString().replace(-, ) ext; // 建议保存到磁盘独立目录不要直接丢进项目发布目录 file.transferTo(new File(uploadDir, newName)); return Map.of(code, 0, msg, 上传成功, data, /upload/ newName); }这里的要点有两个。第一文件名用 UUID 重命名避免中文或特殊字符导致 URL 无法访问第二上传目录要独立于 Tomcat 的webapps目录否则每次重新部署项目上传的商品图片会被一起清掉。更合理的方式是配置一个虚拟路径映射让 Tomcat 把/upload/**请求映射到磁盘上的实际目录。4.2 下单流程里的声明式事务订单与库存必须同时成功用户下单可能是整个系统里最需要谨慎的功能。从业务上讲用户创建订单后必须同时完成两件事插入一条订单记录和一条订单详情记录然后把对应商品的库存减少相应的数量。如果只插入订单而库存没扣后台看到的数据就会失真如果扣了库存但订单失败用户就会白白少了商品。处理这个问题最直接的方式是在 Service 方法上加上Transactional注解Service public class OrderService { Transactional(rollbackFor Exception.class) public void createOrder(OrderCreateDTO dto) { // 1. 计算订单总金额 BigDecimal total calculateTotal(dto.getItems()); // 2. 插入订单主表 Order order new Order(); order.setUserId(dto.getUserId()); order.setTotalPrice(total); order.setStatus(0); // 0 表示新建订单 orderMapper.insert(order); // 3. 批量插入订单明细 for (OrderItem item : dto.getItems()) { item.setOrderId(order.getId()); orderItemMapper.insert(item); // 4. 扣减库存使用带条件更新的 SQL 防止超卖 int updatedRows productMapper.decreaseStock(item.getProductId(), item.getQuantity()); if (updatedRows 0) { throw new RuntimeException(商品库存不足订单创建失败); } } } }在这个方法里只要任何一个orderItemMapper.insert或productMapper.decreaseStock执行失败整个方法抛出异常Spring 就会回滚前面所有已经执行的 SQL保证订单数据和库存数据的一致性。注意rollbackFor Exception.class这个属性Spring 默认只对 RuntimeException 回滚如果你的业务方法抛出的是Exception的受检异常不加这个属性事务不会回滚那样就会出现前文提到的不一致问题。decreaseStock的 SQL 是防超卖的关键。代码可以这么写update iddecreaseStock UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity} /updateWHERE id #{productId} AND stock #{quantity}这个条件让数据库在更新时自行判断库存是否充足。UPDATE语句返回的影响行数如果是 0说明库存不够或者商品不存在Service 层据此抛出异常。不会先把库存查出来再在 Java 里比较因为高并发情况下查出来的stock值可能已经被其他线程修改用条件更新就可以把判断和扣减在一条 SQL 里原子完成。这是经典的乐观锁变体写法比先SELECT再加锁更轻量。4.3 LayUI 搜索与表单校验细节商品列表页通常还有一个搜索框按名称或类别筛选。LayUI 表格通过where参数把搜索词传给后端table.reload(productTable, { where: { keyword: $(#keyword).val() } });对应的 Controller 方法在list里追加一个keyword参数MyBatis 的查询语句变成select idselectPage resultTypecom.xxx.entity.Product SELECT * FROM product where if testkeyword ! null and keyword ! name LIKE CONCAT(%, #{keyword}, %) /if /where LIMIT #{offset}, #{limit} /selectwhere标签会自动处理 AND 前缀问题比如当只有keyword条件时生成的 SQL 不会出现多余的WHERE AND name LIKE...。这是 MyBatis 动态 SQL 里很常用的技巧。如果你看到有人手动写trim去拼接效果是一样的但where更直观。LayUI 的表单验证则依赖lay-verify属性比如商品价格字段设置input nameprice lay-verifyrequired|number placeholder请输入价格 autocompleteoff classlayui-input提交时 LayUI 会自动拦截空值和非法数字减少后端校验压力。但要注意前端校验只是用户体验层面的真正的必填校验还要在后端再做一遍因为接口完全可以被绕过前端直接调用。5. 源码包避坑与上线前必改的四项配置5.1 ZIP 解压乱码与 EOCD 报错的正确处理方式从网络上下载的 ZIP 资源经常会遇到两件事第一是在 Window 系统上解压后文件名出现é»ç这类乱码第二是解压工具直接报Invalid zip archive: could not find EOCD。这两个问题的根源和解决办法都不相同。乱码的根源是 ZIP 文件在打包时使用了 GBK 编码记录文件名而现代解压工具默认按 UTF-8 去解析。Windows 资源管理器自带工具、WinRAR 的老版本都可能出现这种情况。我处理这类源码包的经验是如果在图形工具里解压乱码改用支持编码切换的工具比如 7-Zip 解压时在高级选项里指定 GBK或者先检查文件头部后缀是否真的是 ZIP 格式用命令行确认file flower_project.zip unzip -l flower_project.zip | head -20unzip -l会列出 ZIP 包内的文件清单如果文件名是乱码说明编码有问题如果命令直接报错说找不到中央目录记录那就是文件本身不完整。could not find EOCD的 EOCD 是 End Of Central Directory它位于 ZIP 文件的末尾记录了整个压缩包的目录索引。出现这个错误通常是文件没有下载完下载过程中被中断或者文件被网盘客户端改名了。最直接的验证方式是用tail -c 64看看文件末尾有没有PK\x05\x06结尾tail -c 64 flower_project.zip | xxd | tail -5如果末尾不是PK结尾的一串记录说明文件不完整。此时重新下载并且下载后对比文件大小不要急着解压。顺便提一句这种压缩包损坏的情况和压缩包是否加密无关不需要去找任何密码绕过工具补全文件才是正解。5.2 项目上线前必须检查的四个配置点本地运行成功只是第一步如果把这套系统部署到测试服务器或生产环境下面四个配置点很容易成为定时炸弹。第一jdbc.properties里的useSSLfalse。如果数据库服务器开启了 SSL 强制要求这个参数要改成true或明确指定证书否则连接会被拒绝。本地的开发环境通常没有开启所以默认留false没问题但部署时是否改动取决于 DBA 的策略。第二serverTimezone。正式环境数据库一般部署在托管机房服务器时区可能是UTC而业务上要求订单时间按北京时间显示。最稳妥的做法是连接串里明确写成serverTimezoneAsia/Shanghai然后在数据库侧也把default-time-zone设置一致。否则订单创建时间会差 8 小时排在报表里的日销售额对不上。第三日志配置。SSM 项目普遍用log4j2或logback默认的日志级别可能是DEBUG。如果直接上生产每一次商品列表查询都会打印 SQL 语句日志文件会以肉眼可见的速度膨胀。上线前把根级别调到INFOMyBatis 的 mapper 日志如果必须保留建议单独输出到独立文件并做按天切割。第四会话超时时间。web.xml里默认的session-timeout可能是 30 分钟或更短。鲜花销售系统后台管理员经常需要长时间填写商品信息如果做到一半被踢下线体验很差。把这个值改成 60 或 120单位是分钟session-config session-timeout60/session-timeout /session-config更讲究一点的用 Redis 做 Session 共享这样以后水平扩展多个 Tomcat 实例时用户登录状态不会因为请求落到不同服务器而丢失。但这是后话了对于单机部署的演示项目先确认session-timeout足够宽容即可。5.3 快速验证订单事务回滚的小实验如果你想确认项目里的事务真的生效可以在createOrder方法的库存扣减操作之后手动加一行抛出异常if (true) { throw new RuntimeException(测试事务回滚); }然后在数据库里分别记录插入前的订单数和库存数。重新发起一次下单请求观察结果SELECT COUNT(*) FROM orders; SELECT COUNT(*) FROM order_item; SELECT id, stock FROM product WHERE id 1;如果事务开启正确你会发现订单表、订单明细表的数量没有增加商品库存也没有变化所有操作都回滚得干干净净。如果发现订单插进去了但库存没扣或者订单没插但库存扣了那就去检查Transactional是否写在了实现类的方法上而不是接口方法上——Spring 基于动态代理实现事务只有外部通过代理对象调用带注解的方法时事务才生效。如果在同一个类的内部this.createOrder()调用事务注解会失效这是 SSM 开发里最隐蔽的问题之一。验证完成后记得把测试代码删掉。这套系统的设计思路其实可以迁移到任何库存敏感型业务里重点掌握的是分层结构、声明式事务和条件更新这三件事以后不管接手什么样的 SSM 项目你都能很快定位到对应代码。本文还有配套的精品资源点击获取
返回列表