尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

JavaEE SSH 供应链管理系统实战:采购入库库存事务与避坑指南

JavaEE SSH 供应链管理系统实战:采购入库库存事务与避坑指南 简介这份资源是面向计算机相关专业毕业设计场景的百货中心供应链管理系统完整项目采用JavaEE技术栈结合SSH框架与MySQL数据库实现适合正在准备毕设或课程设计的学生参考与二次开发。压缩包共179个文件约117.58MB涵盖50个Java源文件、26个JSP页面、21个JavaScript脚本、19个XML配置以及SQL建库脚本、properties配置、PPT与MP4辅导视频等源码、文档与讲解视频配套齐全便于理解系统整体架构与业务逻辑。项目围绕采购、销售、库存、合作等供应链核心模块展开包含Action、Service、DAO分层实现结构清晰适合学习SSH整合开发与数据库设计。目前已有553人学习下载可作为毕设选题的完整参考方案帮助读者快速掌握项目搭建、模块划分与调试排错思路。1. 百货中心供应链管理系统从一张采购单到一套能跑起来的 JavaEE 工程百货中心的供应链和便利店完全不是一个量级。一个中型百货中心SKU 动辄几万条供应商几百家采购、入库、调拨、退货、结算五条线并行跑任何一条线靠 Excel 和微信群撑着月底对账一定翻车。这个标题讲的就是用 JavaEE SSHStruts2 Spring Hibernate MySQL 搭一套能管住这些流程的供应链管理系统并且带源码和文档。它解决的核心问题很具体采购单从谁发起、经过谁审批、入库后库存怎么变、供应商结算数据从哪来。适合两类人——一是课程设计或毕设阶段需要一套完整 JavaWeb 项目练手的同学二是中小型零售企业里想用成熟技术栈自建进销存后台的开发者。SSH 这套组合今天看不算新但它的分层结构清晰、资料多、排错路径成熟作为供应链这类业务逻辑偏重的系统反而比追新框架更稳。下面我按实际落地的顺序把选型、建表、核心模块、避坑和验证一条条拆开讲。2. SSH 三层怎么分工为什么供应链系统适合这套老组合2.1 Struts2 管请求、Spring 管对象、Hibernate 管持久化SSH 不是三个框架简单叠加而是各管一段。Struts2 站在最前面接 HTTP 请求把表单参数封装成 Action 的属性再决定跳哪个页面Spring 在中间做 IoC 容器和事务管理Action、Service、DAO 这些对象都由它创建和注入事务边界也压在 Service 层Hibernate 在最底下把 Java 对象和 MySQL 表做映射开发者写的是session.save(purchaseOrder)而不是拼 SQL。供应链系统的业务特点决定了这个分工很合适。采购单审批、库存扣减、结算生成这些操作往往要跨多张表必须放在一个事务里。Spring 的声明式事务只要在 Service 方法上加TransactionalHibernate 的 Session 就会跟着事务走中间任何一步抛异常整体回滚。如果用裸 JDBC这些回滚逻辑得手写出错概率高得多。分层之后改需求的影响面也可控。比如采购单要加一个「紧急程度」字段改的是实体类、映射文件和 JSPService 和 Action 基本不动。这就是为什么很多企业后台至今还在用 SSH——不是技术先进是维护成本低。2.2 环境搭建JDK、Tomcat、MySQL 的版本对齐动手前先把版本定死SSH 对版本比较敏感。我一般用这套组合JDK 8、Tomcat 8.5、MySQL 5.7 或 8.0、Struts2 2.5.x、Spring 4.3.x、Hibernate 5.2.x。JDK 别上 11 以上Struts2 老版本在模块化上有坑。MySQL 安装时字符集一定要选utf8mb4供应链系统里商品名、供应商名经常有生僻字和特殊符号用utf8存进去会变问号。安装完先建库CREATE DATABASE supply_chain DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER sc_userlocalhost IDENTIFIED BY Sc2024; GRANT ALL PRIVILEGES ON supply_chain.* TO sc_userlocalhost; FLUSH PRIVILEGES;建库时显式指定utf8mb4是因为 MySQL 8.0 默认虽然是 utf8mb4但 5.7 默认是 latin1不指定后面中文全乱。单独建用户而不是直接用 root是为了后面配连接池时权限清晰也方便排查「认证失败」类问题——如果连不上先确认用户主机是localhost还是%本地开发用localhost就够。Tomcat 这边把项目打成 war 丢进 webapps 是最省事的部署方式。启动前检查conf/server.xml里的端口有没有被占用8005、8080、8009 三个端口冲突是新手最常见的启动失败原因。2.3 用 Maven 把依赖钉死避免 jar 包打架SSH 项目最容易出的问题就是 jar 包冲突尤其是 Struts2 和 Hibernate 都依赖的 javassist、commons-lang 这些。用 Maven 统一管理版本写在pom.xml里比手动往WEB-INF/lib塞 jar 靠谱得多。dependencies !-- Struts2 核心注意排除 javassist 避免和 Hibernate 冲突 -- dependency groupIdorg.apache.struts/groupId artifactIdstruts2-core/artifactId version2.5.30/version exclusions exclusion groupIdjavassist/groupId artifactIdjavassist/artifactId /exclusion /exclusions /dependency !-- Spring 整合 Hibernate 的 ORM 模块 -- dependency groupIdorg.springframework/groupId artifactIdspring-orm/artifactId version4.3.30.RELEASE/version /dependency !-- MySQL 驱动8.0 用 com.mysql.cj.jdbc.Driver -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency /dependencies这里的关键动作是exclusions排除 javassist。Struts2 自带的 javassist 版本往往和 Hibernate 需要的不一致不排除的话启动时报NoSuchMethodError或者ClassNotFoundException而且报错位置飘忽很难定位。MySQL 驱动 8.0 之后类名从com.mysql.jdbc.Driver变成com.mysql.cj.jdbc.Driver连接串还要加时区参数否则报时区错误。3. 供应链核心表怎么设计采购、库存、供应商三张主线的字段取舍3.1 采购单主表和明细表的一对多拆分供应链系统的数据模型核心就三条线采购、库存、供应商。采购单是最典型的头明细结构一张采购单对应多条商品明细必须拆成两张表。-- 采购单主表记录单据级别的信息 CREATE TABLE t_purchase_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 采购单号业务唯一, supplier_id BIGINT NOT NULL COMMENT 供应商ID, total_amount DECIMAL(12,2) DEFAULT 0.00 COMMENT 采购总金额, status TINYINT DEFAULT 0 COMMENT 0待审 1已审 2已入库 3已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, audit_time DATETIME COMMENT 审批时间未审批为NULL ) ENGINEInnoDB; -- 采购明细表一行一个商品 CREATE TABLE t_purchase_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL COMMENT 关联主表ID, goods_id BIGINT NOT NULL, quantity INT NOT NULL DEFAULT 0, unit_price DECIMAL(10,2) NOT NULL, INDEX idx_order (order_id) ) ENGINEInnoDB;主表存单据级信息明细表存商品级信息这是头明细结构的标准做法。order_no加唯一索引是因为采购单号是业务主键重复了会导致对账混乱。status用 TINYINT 而不是字符串查询和索引效率更高但要在代码里维护好状态枚举别让 0 和 1 的含义在前后端对不上。total_amount冗余存一份是为了列表页不用每次去 sum 明细代价是明细变动时要同步更新这个同步逻辑必须放在同一个事务里。3.2 库存表为什么不能只存一个数量新手最容易犯的错是库存表只设计一个quantity字段。实际业务里库存至少有「可用量」和「占用量」两个概念。采购单审批通过但还没入库时这批货是「在途」销售出库但还没发货时是「占用」。只存一个数量根本算不清。CREATE TABLE t_stock ( id BIGINT PRIMARY KEY AUTO_INCREMENT, goods_id BIGINT NOT NULL UNIQUE, available_qty INT DEFAULT 0 COMMENT 可用库存, locked_qty INT DEFAULT 0 COMMENT 锁定库存如已下单未出库, in_transit_qty INT DEFAULT 0 COMMENT 在途库存采购已审未入库, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB;goods_id加唯一约束保证一个商品只有一条库存记录避免并发插入产生重复行。三个数量字段分开存业务含义清晰真正能卖的是available_qty下单时从可用挪到锁定出库时锁定减少入库时在途减少、可用增加。update_time用ON UPDATE CURRENT_TIMESTAMP自动维护排查库存异常时能看出最后一次变动时间。3.3 供应商表和商品表的关联设计供应商和商品是多对多关系一个供应商供多种商品一种商品也可能有多个供应商。中间表不能省。CREATE TABLE t_supplier ( id BIGINT PRIMARY KEY AUTO_INCREMENT, supplier_name VARCHAR(100) NOT NULL, contact_person VARCHAR(50), phone VARCHAR(20), status TINYINT DEFAULT 1 COMMENT 1合作中 0已停用 ) ENGINEInnoDB; -- 供应商-商品关联表 CREATE TABLE t_supplier_goods ( id BIGINT PRIMARY KEY AUTO_INCREMENT, supplier_id BIGINT NOT NULL, goods_id BIGINT NOT NULL, supply_price DECIMAL(10,2) COMMENT 该供应商的供货价, UNIQUE KEY uk_sup_goods (supplier_id, goods_id) ) ENGINEInnoDB;关联表上建(supplier_id, goods_id)联合唯一索引防止同一对关系重复插入。supply_price放在关联表而不是商品表是因为同一商品不同供应商报价不同采购时按供应商取价。供应商的status字段很关键停用的供应商不能出现在新建采购单的下拉列表里但历史单据还要能查到它的名字所以用逻辑删除而不是物理删除。4. 采购到入库的完整链路Service 层事务和 Hibernate 映射怎么写4.1 实体类与 hbm.xml 映射的关键配置Hibernate 的映射有两种写法注解和 XML。SSH 老项目里 XML 更常见因为改映射不用重新编译。以采购单主表为例!-- PurchaseOrder.hbm.xml -- hibernate-mapping class namecom.sc.entity.PurchaseOrder tablet_purchase_order id nameid columnid generator classnative/ /id property nameorderNo columnorder_no length32 not-nulltrue/ property nametotalAmount columntotal_amount typebig_decimal/ property namestatus columnstatus typebyte/ !-- 一对多一张采购单对应多条明细cascade 让保存主表时级联保存明细 -- set nameitems cascadesave-update lazytrue inversetrue key columnorder_id/ one-to-many classcom.sc.entity.PurchaseItem/ /set /class /hibernate-mappinggenerator用native让 Hibernate 根据数据库自动选自增策略MySQL 下就是 auto_increment。inversetrue表示关联关系由明细表那一端维护主表不主动发 update避免多一条无用 SQL。cascadesave-update让保存采购单时自动保存明细但注意别配成all否则删主表会连带删明细采购单一般不允许物理删除。lazytrue延迟加载列表页只查主表进详情页才查明细性能差别很大。4.2 采购单审批与入库的事务处理审批和入库是两个独立动作但入库会同时改采购单状态和库存必须在一个事务里。Service 层写法Service public class PurchaseServiceImpl implements PurchaseService { Autowired private PurchaseOrderDao orderDao; Autowired private StockDao stockDao; // 审批只改状态简单事务 Transactional public void audit(Long orderId) { PurchaseOrder order orderDao.get(orderId); if (order.getStatus() ! 0) { throw new BizException(单据状态不允许审批); } order.setStatus((byte) 1); order.setAuditTime(new Date()); orderDao.update(order); } // 入库改状态 加库存 减在途多表操作必须同一事务 Transactional public void stockIn(Long orderId) { PurchaseOrder order orderDao.get(orderId); if (order.getStatus() ! 1) { throw new BizException(只有已审批的单据才能入库); } for (PurchaseItem item : order.getItems()) { // 悲观锁锁定库存行防止并发入库导致数量错乱 Stock stock stockDao.getForUpdate(item.getGoodsId()); stock.setAvailableQty(stock.getAvailableQty() item.getQuantity()); stock.setInTransitQty(stock.getInTransitQty() - item.getQuantity()); stockDao.update(stock); } order.setStatus((byte) 2); orderDao.update(order); } }Transactional加在方法上Spring 会在方法进入时开事务、正常返回时提交、抛 RuntimeException 时回滚。注意默认只对运行时异常回滚如果抛的是受检异常要显式配rollbackFor。入库时用getForUpdate走SELECT ... FOR UPDATE加行锁是因为同一商品可能被多张采购单同时入库不加锁会出现两个线程读到同一个旧值、各自加一遍、最后少加一次的情况。这是库存系统最典型的并发坑血泪经验。4.3 Struts2 Action 接收参数与返回 JSON前端提交采购单Action 负责接参数、调 Service、返回结果。Struts2 的 Action 是单例多线程的成员变量会被并发访问所以业务数据不要放成员变量用 ModelDriven 或者方法参数传。public class PurchaseAction extends ActionSupport { private Long orderId; private MapString, Object result new HashMap(); Autowired private PurchaseService purchaseService; // 入库接口返回 JSON 给前端 public String stockIn() { try { purchaseService.stockIn(orderId); result.put(code, 200); result.put(msg, 入库成功); } catch (BizException e) { result.put(code, 500); result.put(msg, e.getMessage()); } return json; } public void setOrderId(Long orderId) { this.orderId orderId; } public MapString, Object getResult() { return result; } }Action 里只做参数接收和结果封装业务逻辑全在 Service这是分层的底线。result用 Map 装返回数据配合 Struts2 的 json 插件序列化。注意 Action 由 Spring 管理时要在struts.xml里配class为 Spring bean 名并且 Action 的 scope 要设成 prototype否则单例下成员变量会串数据。5. 部署与联调避坑认证失败、乱码、事务不生效的排查清单5.1 数据库连不上从驱动类名到连接串参数现象启动 Tomcat 报Cannot create PoolableConnectionFactory或Access denied for user。原因通常有三个。一是驱动类名写错MySQL 8.0 必须用com.mysql.cj.jdbc.Driver写老的com.mysql.jdbc.Driver会警告甚至失败。二是连接串没加时区报The server time zone value is unrecognized解决是在 URL 后加?serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8mb4。三是用户权限Access denied要确认用户主机是localhost还是%以及密码有没有特殊字符需要转义。排查顺序先看异常第一行再核对驱动类名最后用命令行mysql -u sc_user -p手动连一次能连上说明是配置问题不是权限问题。5.2 中文乱码三个地方都要设对现象商品名存进数据库变成问号或者页面显示乱码。原因分三层。数据库层建库建表要utf8mb4连接层JDBC URL 要带characterEncodingutf8mb4应用层JSP 页面头部要% page contentTypetext/html;charsetUTF-8 %Struts2 要在struts.properties里配struts.i18n.encodingUTF-8。三层缺一层就乱码。排查方法直接在 MySQL 命令行SELECT看数据本身对不对如果命令行显示正常但页面乱码问题在应用层如果命令行就是问号问题在建库或连接层。5.3 事务不生效自调用和配置扫描的坑现象入库时库存改了但采购单状态没改或者抛异常后数据没回滚。原因最常见的是 Service 内部方法自调用。比如stockIn里直接调this.audit()这样走的是对象内部调用不经过 Spring 代理Transactional失效。解决是把被调方法抽到另一个 Service或者用AopContext.currentProxy()拿代理对象调。另一个原因是事务注解没被扫描到检查applicationContext.xml里tx:annotation-driven和组件扫描的包路径有没有覆盖到 Service。排查时可以在方法里打日志看事务是否真的开启。5.4 懒加载异常Session 关闭后访问关联对象现象列表页正常点进详情页报LazyInitializationException: could not initialize proxy - no Session。原因是 Hibernate 的 Session 在 Service 方法结束后就关了而 JSP 里还在访问order.getItems()这种懒加载集合。解决办法有三种一是把lazy改成false简单但性能差二是在 Service 里用Hibernate.initialize(order.getItems())提前加载三是配OpenSessionInViewFilter让 Session 在请求全程保持打开。我一般用第二种按需加载可控。第三种虽然省事但会掩盖性能问题不推荐在生产用。5.5 并发入库数量对不上锁的粒度和顺序现象压测时库存总数比实际入库少。原因是没加锁或者锁粒度不对。前面提到的SELECT ... FOR UPDATE是行锁但要注意两点一是必须在事务里才生效自动提交模式下锁立刻释放二是多张采购单入库时如果商品顺序不一致可能死锁。解决是入库前对商品 ID 排序按固定顺序加锁。另外FOR UPDATE锁的是索引行如果goods_id没索引会锁全表所以库存表的goods_id唯一索引不能省。6. 让这套系统真正可用从能跑到能扛的几个进阶动作系统能跑起来只是第一步供应链系统真正上线还要过几关。第一关是分页。采购单列表数据量大了以后一次性查全表会拖垮内存。Hibernate 的分页用Query.setFirstResult()和setMaxResults()配合count查询算总数。注意setFirstResult在数据量大时性能差因为 MySQL 的LIMIT offset, size在 offset 很大时要扫描前面所有行优化方向是用游标或者记录上次最大 ID。第二关是状态机校验。采购单的状态流转必须是单向的待审→已审→已入库不能从已入库退回待审。我一般把合法流转写成一个 Map每次改状态前查一下不合法直接抛异常。这样比散落在各处的 if 判断好维护也方便加日志。第三关是操作日志。供应链系统出了库存差异必须能查到是谁在什么时候改了什么。建一张t_oper_log表记录操作人、操作类型、单据 ID、变更前后值。用 Spring AOP 在 Service 方法上切自动记录比手动埋点省事。验证这套系统是否真的可用我有个习惯造一批测试数据跑一遍完整链路——建供应商、建商品、建采购单、审批、入库、查库存、再退货、再查库存看每一步的数字对不对。特别是退货很多人只测正向流程退货时库存回滚逻辑最容易漏。最后用 JMeter 并发调入库接口看库存总数是否等于所有入库单数量之和对不上就说明锁有问题。这套 SSH 方案不新但供应链这类业务逻辑重、事务要求高的系统稳定比时髦重要。我踩过最深的坑是早期没加行锁压测时库存少了几十件查了两天才定位到并发。后来养成习惯凡是改数量的操作先想清楚锁在哪。希望帮到你。本文还有配套的精品资源点击获取
返回列表