ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Java SCM供应链项目实战:从领域建模到库存联动

2026/9/17 2:24:15 拓冰建站 浏览量
Java SCM供应链项目实战:从领域建模到库存联动 简介面向Java开发者的SCM供应链项目源码包涵盖WMS仓库管理等典型业务模块适合学习企业级进销存、订单与库存协同开发的初中级工程师参考。包体共11161个文件压缩后85.53MB以class、java、jsp、js、css、xml、property等为主其中java源文件2215个、页面与脚本文件分别提供前端交互和后台逻辑实现便于按模块对照研读。目前已有551人学习下载内容包含完整项目工程、配置文件及数据库相关文件目录结构覆盖controller、service等分层设计可帮助理解供应链场景下单据流转与库存同步的实际编码方式。对于希望快速上手SSM或Spring类框架搭建后台管理系统的开发者这套源码能提供较直接的项目范式与排错思路。1. JAVA SCM供应链项目代码到底要解决什么接手的供应链项目如果只有一张采购订单表和一个库存表那它根本跑不起来。真实SCM项目代码的难点不在CRUD而是把供应商、采购、库存、销售、回款这条链路上的数据状态挪到同一套规则里。这个规则叫“单据流”也就是每笔业务必须由上游单据驱动下游回写上游状态。Java里最常用的落地方式是Spring Boot MyBatis配合Redis做库存预占再靠事务和幂等把并发打干净。这篇文章不聊ERP巨型系统只讲一个中型供应链项目代码的拆解和可复现写法适合准备接手或者重写SCM模块的后端工程师也适合想在前端讨论API契约时能反过来把服务端边界说清楚的人。2. 供应链项目的领域建模从业务流里抽出Java实体和表结构2.1 先画出跨部门的供应链流程再把流程转成状态机做SCM项目代码第一件事不是建Maven工程而是把业务部门的语言翻译成对象和状态。常见流程是供应商报价 → 生成采购订单 → 供应商发货 → 仓库收货入库 → 库存增加 → 客户下单 → 锁定库存 → 出库扣减 → 生成结算单。这个流程里最关键的是“库存变更”因为每次变动都要有来源单据否则账实不符时根本没法溯源。我会用一张手绘图或者Excel表把流程画出来标出每个节点上谁在操作、会产生什么单证。然后把这些单证落成Java实体PurchaseOrder、PurchaseOrderItem、StockMovement、SalesOrder、Supplier。这里的反直觉点是不要一开始就建一堆字典表而是先把状态机定义清楚。例如采购订单状态用0:草稿 1:已确认 2:已发货 3:已入库 4:已关闭库存变动类型用INBOUND/OUTBOUND/ADJUST。这样在写代码时每个方法都带着状态流转意图。2.2 从ER模型映射到Java类的几个关键取舍给出一个简化后的采购订单实体注意字段类型和注释。public class PurchaseOrder { private Long id; private String orderNo; // 业务单号如 PO20240601001 private Long supplierId; private Integer status; // 0草稿 1已确认 2已发货 3已入库 4已关闭 private BigDecimal totalAmount; // 金额必须用BigDecimal不用double private LocalDateTime confirmTime; private LocalDateTime warehouseInTime; // getter/setter 省略 }为什么订单号要用字符串而不是数据库自增ID因为SCM系统里财务、仓库、供应商用的单号口径必须一致自增ID会暴露业务量也对接不稳定。状态字段用Integer而不是String因为数据库索引对数字更友好代码里用常量类管理避免魔法值。金额一定用BigDecimal这是Java的强制要求避免二进制浮点误差。2.3 表结构设计要防住并发和审计两个打点采购订单和库存表SQL大致如下CREATE TABLE purchase_order ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(64) NOT NULL COMMENT 业务单号, supplier_id bigint NOT NULL, status tinyint NOT NULL DEFAULT 0, total_amount decimal(18,2) NOT NULL DEFAULT 0.00, version int NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, create_time datetime NOT NULL, update_time datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_supplier_status (supplier_id,status) ) ENGINEInnoDB COMMENT采购订单表;version是乐观锁用的后面章节的库存扣减会用到。uk_order_no是防重复的关键业务上同一次创建被重复提交时数据库会直接报错比代码里查一遍更可靠。idx_supplier_status覆盖“查某个供应商的订单列表”这个高频查询。下面表格是字段选型时最常遇到的争议点字段场景推荐类型原因金额decimal(18,2)精度可控避免浮点误差状态tinyint索引空间小枚举可读性靠代码补单据编号varchar(64)加唯一索引业务侧幂等查询友好库存数量int或bigint避免小数库存若需要小数用decimal计量单位varchar(20)不做复杂单位换算过度设计会拖慢系统3. 用Spring Boot搭建可维护的SCM项目骨架3.1 Maven多模块划分为什么把mapper单独抽一层供应链项目代码不像简单博客系统它天然有多个调用方采购后台、仓库PDA、报表服务。我把模块拆成scm-common、scm-dal、scm-service、scm-web。scm-dal里放MyBatis的Mapper和XMLscm-service放业务实现scm-web只放Controller和VO。这样前端工程师拿到scm-web模块就可以直接看API约定不用翻数据库访问代码。pom.xml的模块声明通常写成这样modules modulescm-common/module modulescm-dal/module modulescm-service/module modulescm-web/module /modulesscm-dal模块的依赖只暴露给scm-servicescm-web不允许直接引用Mapper。这样做的原因是避免前端页面需求改动时逼着业务层跟着换同时让单元测试能只加载scm-dal做数据库联调。3.2 配置文件里的三个必调参数连接池、Redis序列化、MyBatis下划线映射一个典型的application.yml核心内容如下spring: datasource: url: jdbc:mysql://localhost:3306/scm_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: secret hikari: maximum-pool-size: 20 minimum-idle: 5 redis: host: localhost port: 6379 database: 3 mybatis: mapper-locations: classpath:mappers/*.xml configuration: map-underscore-to-camel-case: trueserverTimezone如果不设置MySQL 8 和 Java 17 之间会出现时间差八小时的问题。maximum-pool-size一般设为核心数的两倍数据库连接数不是越大越好超过阈值反而会拖垮数据库。map-underscore-to-camel-case必须开启这样数据库的supplier_id会自动映射到supplierId少写一堆Results注解。3.3 接手Java Spring Boot项目代码时先看什么热词里不少人在搜“前端开发工程师接收一个Java SpringBoot项目后端可以直接上手改代码吗”我的答案是能但有顺序。先看pom.xml确认Spring Boot版本和依赖再看application.yml确认数据库和Redis地址然后看Controller层路由最后才看业务实现。最忌讳的是打开service层从头读因为供应链项目的Service往往很大一上来就会被事务和状态流转绕晕。在切换数据库连接环境时注意spring.sql.init之类会自动执行脚本的配置在测试环境开着没问题生产环境尽量关掉否则每次启动都会把初始化SQL再跑一遍可能直接清掉生产表。4. 用Java实现采购入库到库存扣减的联动逻辑4.1 采购入库使用事务和乐观锁更新库存供应链系统最怕库存账和账面不一致。采购入库的操作不是直接insert一条库存记录而是先更新库存表再写流水。用乐观锁防止并发重复入库Transactional public void warehouseIn(StockMovement movement) { int updated stockMapper.decreaseStock( movement.getSkuId(), movement.getChangeQty(), System.currentTimeMillis()); if (updated 0) { throw new OrderServiceException(库存更新失败请重试); } stockMovementMapper.insert(movement); purchaseOrderMapper.updateStatus(movement.getOrderNo(), 3); }对应Mapper XML里的更新语句update iddecreaseStock UPDATE stock SET available_qty available_qty - #{changeQty}, update_time #{updateTime} WHERE sku_id #{skuId} AND available_qty #{changeQty} /update关键点在于WHERE条件里的available_qty #{changeQty}这是数据库层面的乐观锁。如果两个请求同时扣同一批库存数据库的锁机制会保证只有一个成功另一个更新行数为0代码里直接抛异常。Transactional保证库存减少和流水写入在同一个事务后一步失败时库存回滚不会出现流水和库存对不上的情况。4.2 用Redis做订单锁库存的Java实现销售订单创建时如果直接扣MySQL库存行锁在高并发下会拖垮数据库。常见做法是用Redis先预占库存再异步写数据库。代码里用RedisTemplate的increment做预减public Boolean tryLockStock(Long skuId, Integer qty, String orderNo) { String key scm:stock:lock: skuId; Long remain redisTemplate.opsForValue().increment(key, -qty); if (remain ! null remain 0) { redisTemplate.opsForSet().add(scm:order:lock: orderNo, skuId : qty); return true; } else { // 回滚本次预占 redisTemplate.opsForValue().increment(key, qty); return false; } }这里用increment的负值语义来预占它有两个注意点一是Redis的key必须配置过期时间比如24小时避免锁库存的key永远不释放二是increment返回的是Long序列化方式不对时会出现“不是integer或out of range”的报错原因多半是RedisTemplate默认使用JDK序列化解决方法是设置String序列化器来存数字RedisTemplateString, Object template new RedisTemplate(); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setHashValueSerializer(new StringRedisSerializer());4.3 事务回滚和幂等性控制SCM代码最容易踩的坑表格里整理了三种常见幂等方案方案适用场景实现方式失败表现唯一索引采购订单创建order_no加 unique key插入报错可根据错误码返回“重复提交”状态机校验入库/出库确认WHERE status N更新更新行数为0抛业务异常Redis预占订单高并发锁库存increment负数 回滚预占回滚返回失败在实际项目里这三种方案会组合使用。例如采购入库时先查订单状态再执行更新更新行数为0说明已经被别人处理过直接抛异常。不要把“查状态”和“更新状态”分开因为两个请求可能同时通过校验必须让更新语句自己带条件。5. 供应链数据分析用SQL和Java Stream做供应商与库存分析5.1 用SQL统计供应商交付及时率做报表时不要把所有数据查出来到Java里算应该在SQL里完成聚合。下面是统计每家供应商准时交付率的查询SELECT supplier_id, COUNT(*) AS total_order_count, SUM(CASE WHEN actual_warehouse_in_time plan_warehouse_in_time THEN 1 ELSE 0 END) AS on_time_count, ROUND( SUM(CASE WHEN actual_warehouse_in_time plan_warehouse_in_time THEN 1 ELSE 0 END) / COUNT(*), 4 ) AS on_time_rate FROM purchase_order WHERE create_time 2025-01-01 GROUP BY supplier_id HAVING COUNT(*) 1 ORDER BY on_time_rate DESC;这里的actual_warehouse_in_time和plan_warehouse_in_time是采购订单表里的两个时间字段如果底层表没存那这个指标就算不出来所以设计表结构时要提前规划。HAVING字段过滤的是聚合后的结果不是原始行这点容易写错。供应链数据分析里还有一个常见口径是按订单数算及时率还是按订单金额算及时率两者结论可能完全不同和业务方确认口径后再写代码。5.2 用Java Stream对库存做ABC分类SQL负责聚合内存负责分类。ABC分类的思路是按库存金额从大到小排序累计金额占比前70%的商品为A类70%到90%为B类剩下为C类。用Java Stream实现非常直观ListStockItem items stockMapper.selectAllStockItems(); BigDecimal totalAmount items.stream() .map(StockItem::getStockAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); ListStockItem sorted items.stream() .sorted(Comparator.comparing(StockItem::getStockAmount).reversed()) .toList(); BigDecimal accumulated BigDecimal.ZERO; for (StockItem item : sorted) { accumulated accumulated.add(item.getStockAmount()); double ratio accumulated.doubleValue() / totalAmount.doubleValue(); if (ratio 0.7) { item.setAbcCategory(A); } else if (ratio 0.9) { item.setAbcCategory(B); } else { item.setAbcCategory(C); } }reduce(BigDecimal.ZERO, BigDecimal::add)用来求和避免循环里写totalAmount 时精度丢失。sorted里的reversed()容易把金额排序写成降序比较器返回-1表示前一个小于后一个这样排出来是升序所以要格外注意。给每个item打标签以后批处理更新时一次update更新一个分类比逐条更新效率高得多。5.3 导出海量库存数据时怎么防止内存爆炸不要直接select * from stock再写到Excel几万条就会把堆内存撑爆。我用的是MyBatis的流式查询按游标每次拿500条try (SqlSession session sqlSessionFactory.openSession()) { StockMapper mapper session.getMapper(StockMapper.class); CursorStockItem cursor mapper.scanAllStock(); IteratorStockItem iterator cursor.iterator(); while (iterator.hasNext()) { StockItem item iterator.next(); writer.writeRow(item); } }使用流式查询时数据库连接会一直被占用所以要注意这个操作期间不要同时跑其他慢查询否则连接池耗尽。导出的响应建议用StreamingResponseBody这样HTTP响应也会边写边发而不是先把整个文件写到内存。6. 上线前必做的验证与排查技巧6.1 用Arthas trace找到SCM服务里的慢方法供应链项目代码里最隐蔽的问题不是报错而是“某些请求偶尔卡一下”。我会用Arthas在测试环境直接跟踪方法调用耗时java -jar arthas-boot.jar trace com.example.scm.service.PurchaseOrderServiceImpl warehouseIn执行后每次调用warehouseIn都会打印该方法内部每个子方法的耗时。看到stockMapper.decreaseStock耗时超过200毫秒时直接去数据库看执行计划。大多数慢查询都出在idx_supplier_status这类联合索引没有命中或者available_qty #{changeQty}的更新语句把行锁范围扩得太大。6.2 检查数据库连接池和Redis连接的一个快速命令上线前我会同时观察两个指标连接池活跃数、Redis慢日志。连接池活跃数通过actuator/prometheus接口抓Redis慢日志在客户端里执行SLOWLOG GET 10 SLOWLOG LEN如果发现慢日志里有大量scm:stock:lock:*相关的操作说明单个锁key竞争太激烈需要做分片例如把skuId加随机后缀拆成100个桶。这里后一小时的数据如果持续增长就说明缓存击穿了。6.3 最后检查点本地代码改动被覆盖怎么办很多人在用Idea pull项目后发现自己本地改的代码丢失了这多半是pull之前没有Commit或Stash。对SCM这类多人维护的项目建议在改前强制stash并立刻创建自己的分支。如果改动已经丢失先用Git reflog找到之前的HEADgit reflog git checkout -b recovery-branch HEAD{2}这个命令能把被覆盖前的提交恢复到新分支之后再手动比对差异。这个技巧在编码重启前用不到但遇到同事推了一个错误的分支覆盖到你本地时它能救回半天工作量。以上是Java SCM供应链项目代码从领域建模、工程骨架、核心交易到数据分析与排查的一条完整落地路径。实际项目的复杂度总会比描述的高但只要单据流、状态机、乐观锁这三个底子打得牢后续加供应商协同、多仓库存也只是在这个骨架上做扩展。本文还有配套的精品资源点击获取