
欢迎来到预见猿份,本站项目均为站长原创,学习中有问题可直接提交给站长老苗解决(微信:mrt_0607)。
苗润土老师,20余年一线项目经验,2014年加入黑马,星辰wms、云岚到家、学成在线项目作者,历任高级讲师、教学主管及课程研究员。 b站老苗
第三章 出库管理
学习目标
day01:
- 能够说出出库业务流程
- 能够说出出库单的表结构设计
- 能够说出分配库存的实现流程
- 能够开发分配库存接口
- 能够测试通过分配库存接口
- 能够说出波次管理的表设计
- 能够说出策略模式的实现方法
- 能够说出责任链模式的实现方法
day02:
- 能够开发创建波次功能
- 能够开发创建拣货任务接口
- 能够测试拣货接口
- 能够测试分拣接口
- 能够测试拣货的整体流程
day03:
- 能够理解按波次创建包裹流程
- 能够测试整个打包流程
- 能说出异步生成运单的流程
- 能说出打印电子面单的流程
- 能够对接顺丰测试申请运单接口
- 能够使用定时任务加多线程生成运单
day04:
- 能够开发打印电子面单接口
- 能够说出synchronized与分布式锁的区别
- 能够使用Redisson实现分布式锁
- 能够说出基于AOP实现分布式锁的方法
- 能够测试发货的流程
- 能够说出拣货路径规划的技术方案
- 能够测试通过拣货路径规划功能
1 需求分析
1.1 出库流程
出库指货物从仓库发出,交付给客户或下一环节的过程。主要分为以下类型:
业务场景
- 销售出库:根据客户订单发货(B2B/B2C)。
- 生产领料:车间从仓库领取原材料。
- 调拨出库:将货物调往其他仓库。
- 报废出库:损坏或过期货物移出仓库。
关键流程
- 创建出库单:WMS接收订单(如ERP同步的销售订单),创建出库单。
- 分配库存:系统分配库存(先进先出策略等)。
- 拣货:按订单需求从库位拣取货物,采用波次拣选策略。
- 打包:对拣货商品进行打包,核对订单与实物,打包并贴物流标签。
- 发货:交接给承运商,更新物流信息,库存扣减。
下边介绍电商平台出库流程:

- 电商系统接收订单:用户下单后,电商系统接收并存储订单信息。
- 订单审核(系统自动):电商系统对订单进行审核,包括检查库存、地址有效性、用户信用等,确保订单合规。
- 订单信息推送至WMS:审核通过的订单信息通过API接口推送至WMS系统。
- WMS生成出库单:WMS系统根据订单信息生成出库单。
- WMS分配库存:为出库单分配库存。
- WMS生成波次单:WMS根据波次规则对订单批量生成波次单,比如对单品单件的订单组成一个波次。
- WMS生成波次拣货任务:根据波次单生成波次拣货任务。
- 拣货员拣货:拣货员使用PDA设备根据波次拣货单的指示进行拣货。
- 拣货完成:拣货完成后,货品统一放在打包区。
- 打包:对拣货完成的商品进行打包。
- 打印电子面单:对打包好的商品进行称重和扫描,生成电子面单 ,并将电子面单贴到包裹上
- WMS将出库信息回传电商系统:WMS系统将出库信息(包括物流信息)回传至电商系统。
- 电商系统更新订单状态为“已出库”:电商系统根据WMS系统回传的出库信息更新订单状态。
- 物流商接收货物,确认无误,进行运输。
- 物流信息同步至电商系统物流跟踪页面:物流商将物流信息同步至电商系统,用户可以在电商系统的物流跟踪页面查看物流信息。
- 客户签收商品:客户签收商品,物流信息同步至wms系统、电商系统。
1.2 界面原型
1)创建出库单
与入库单类似,从仓库出库需要创建出库单,可以手工创建,也可以通过OMS或电商系统将订单信息通过接口传给WMS系统。
本章节暂时使用手工创建出库单,后面会讲解接口远程调用的方法。
出库单包括出库单信息和出库单明细:
出库单
编号:OBD001、仓库:中原1号仓库、货主:北京传智教育
出库单明细:
商品编码:C01、出库数量:100件
商品编码:C02、出库数量:50件、批号:202201
界面原型:


2)分配库存
- 按先到期先出库策略分配库存
先到期先出策略:谁的过期时间先到谁先出库,过期时间一样的按先进先出策略。
商品在入库时记录了入库商品的保质期以及入库时间,首先根据出库单的商品查询对应的库存,按保质期升序排序,保质期一样的按入库单升序排序。
查询到库存列表,按商品的出库数量依次对库存列表中的库存进行分配,分配库存即锁定库存,库存锁定后不允许再销售。
库存表有三个字段:在库数量、可用数量、分配数量
分配库存:减少可用数量,增加分配数量。
- 指定批号分配库存
在填写出库单明细时可以指定商品批号,这种情况就要从库存中找到该批号的库存进行分配。
举例:
出库单OBD001 分配库存结果
商品编码:C01、分配数量:50件、储位:Z1A-01-01
商品编码:C01、分配数量:50件、储位:Z1A-01-02
商品编码:C02、分配数量:50件、储位:Z1A-01-03、批号:202201
界面原型:

分配结果可通过出库单详情查看:

3)波次管理
在WMS中波次管理是指将一定时间段内的订单按照特定规则分组(称为"波次"),然后对每个波次的订单进行集中拣货处理,提高拣货效率。
仓库中常用的拣货方式如下:
摘果式拣货(Order Picking)
原理:按订单逐个拣选,像从树上摘果子一样,一次完成一个订单的所有商品。
流程:
系统分配订单→拣货员按订单列表遍历仓库→拣选对应商品→直接打包。特点:
- 优点:操作简单,适合订单量少、商品差异大的场景(如B2C零售)。
- 缺点:订单量大时效率低(重复路径多)。
适用场景:小批量、高时效性订单(如电商单件订单)。
播种式拣货(Batch Picking)
原理:将多个订单中相同SKU合并,生成批量拣货任务。先批量拣选所有商品,再按订单分拣(像播种一样分配)。
流程:
合并订单:将多个订单中相同SKU合并,生成批量拣货任务。批量拣货:一次性拣出所有订单需要的商品总量。
分播(播种):将商品按订单分到对应容器(如快递箱)。
- 举例:
订单A需商品X 2个,订单B需X 3个 → 一次性拣X 5个,再分到A、B订单箱中。
特点:
- 优点:减少行走路径,适合订单商品重叠率高的情况(如超市补货)。
- 缺点:需额外分拣区,操作复杂度高。
适用场景:高频、商品重复的订单(如B2B批发)。
波次拣货(Wave Picking)
- 结合摘果和播种:按订单特性(如相同货位、配送时间)分组,分波次处理,平衡效率与时效。
- 流程
分组:系统根据规则(如相同货位、相同配送时间、商品类别等)将订单分组为多个“波次”。
分时处理:每个波次独立拣货,可能采用摘果式、分区式等具体方法。
动态调整:波次可灵活设定(如每小时一波,或按订单量触发)。
举例:
1000个订单根据商品分组统计后,只有100个商品,面对100个商品拣货比面对1000个订单快
上午10点前下单的订单为“波次1”,优先拣货;
生鲜类订单单独分波次,优先处理。
常见波次策略
- 单品单件波次策略
含义:每个波次只包含一种商品,且每个订单中该商品的数量为1。
适用场景:适用于订单中商品种类多但每种商品数量少的情况。
优点:拣货路径简单,效率高。
缺点:订单量较大时,波次数量多,管理复杂。
- 一品2件、一品3件
含义:
- 一品2件:每个波次包含一种商品,每个订单中该商品的数量为2。
- 一品3件:每个波次包含一种商品,每个订单中该商品的数量为3。
适用场景:适用于订单中某些商品需求量为2或3的情况。
优点:减少拣货次数,提高效率。
缺点:商品需求量不固定时,适用性有限。
- 一品波次策略
含义:每个波次只包含一种商品,但每个订单中该商品的数量不限。
适用场景:适用于订单中某些商品需求量较大的情况。
优点:集中处理单一商品,提高拣货效率。
缺点:订单中商品种类多时,波次数量多,管理复杂。
- 尾单策略
含义:尾单策略将未及时处理的订单归入一个波次,通常是在特定时间段结束时执行。例如,每天下午5点将当天未处理的订单集中处理。
适用场景
- 固定发货时间:如电商仓库每天固定时间发货。
- 订单量波动大:订单量不稳定时,尾单策略能有效处理积压订单
优点
- 减少遗漏:确保所有订单按时处理。
- 优化资源:集中处理订单,提升资源利用率。
缺点
- 延迟风险:订单可能延迟到波次执行时才处理。
- 灵活性不足:不适合需要实时处理的场景。
界面原型:


创建成功可在创建波次列表查询:

波次详情:

4) 按波次拣货
创建拣货任务
- 对本波次下的出库单分配库存结果进行汇总(按商品、储位、批号进行汇总)
举例:
出库单OBD001 分配库存结果
商品编码:C01、分配数量:50件、储位:Z1A-01-01
商品编码:C01、分配数量:50件、储位:Z1A-01-02
商品编码:C02、分配数量:50件、储位:Z1A-01-03、批号:202201
商品编码:C02、分配数量:10件、储位:Z1A-01-04,批号:202302
出库单OBD002 分配库存结果
商品编码:C01、分配数量:10件、储位:Z1A-01-01
商品编码:C02、分配数量:40件、储位:Z1A-01-03、批号:202201
商品编码:C02、分配数量:20件、储位:Z1A-01-04
汇总后:
商品编码:C01、分配数量:60件、储位:Z1A-01-01
商品编码:C01、分配数量:50件、储位:Z1A-01-02
商品编码:C02、分配数量:20件、储位:Z1A-01-04
商品编码:C02、分配数量:10件、储位:Z1A-01-04,批号:202302
商品编码:C02、分配数量:90件、储位:Z1A-01-03、批号:202201
- 根据汇总的结果创建拣货任务。
汇总的目的是为了相同位置的库存一起拣货,提高效率。
根据汇总结果,最终创建4个拣货任务。
拣货
按波次为单位进行拣货,按照拣货路径依次去储位拣货。
支持PDA拣货,详细操作流程参考需求文档。
拣货完成本波次下的商品都移到发货储位。
拣货完成:标识波次状态为已拣货。
注意:注意此时并没有标识出库单及出库单明细的状态为已经拣货。
分拣
分拣是在批量拣货(如播种式拣货或波次拣货)后,将已汇总拣选的商品按照原始订单进行再分配和核对,确保每个订单的商品准确无误。
分拣完成:标识出库单及出库单明细状态为已拣货。
5)打包
根据打包策略生成包裹:
常见的打包策略有:
单件发货
- 描述:一个订单只包含一件商品,直接使用合适的包装材料进行打包。
- 适用场景:B2C电商中非常常见,尤其是服装、小家电等标准品。
合单/整单发货
- 描述:一个订单中的所有商品被打包在同一个包裹里。
- 适用场景:订单商品数量少、体积小,或商品属性适合一起包装(如一套图书、一套化妆品)。这是最直观的方式。
拆单/分单发货
描述:一个订单被拆分成多个包裹发出。
触发原因:
- 库存地点不同:商品分布在不同的仓库或库位。
- 商品特性差异:例如,有大件商品(如家具)和小件商品(如螺丝),需要分开由不同的承运商配送。
- 预售/缺货:部分商品有库存,部分需要等待,为了尽快发出有库存的商品,先将订单拆分。
- 包裹重量/体积超限:单个包裹超过快递公司的重量或体积限制。
本项目提供两种打包策略,使用策略模式实现:
标准打包策略:对应 单品单件波次策略,一个订单一个包裹。
按重量拆分包裹策略:对应其它波次策略。
创建包裹流程:
6)发货
- 打包完成调用第三方快递平台为每个包裹生成快递单号
- 打印电子面单,并将电子面单粘贴到包裹上
- 发货,标识出库单已发货
- 扣减库存
- 发货状态及物流状态同步到电商、ERP系统。
2 出库单管理
1) 准备开发环境
后端工程:
提交现有代码到当前分支。
创建新分支: dev_03
切换到新分支:dev_03
从"课程资料/源码/后端"拷贝xincheng-wms-java-course-dev03.zip,解压后将代码覆盖后端工程dev_03分支的代码。
前端工程:
继续使用第二章的代码
如果出现pom.xml中 SF-CSIM-EXPRESS-SDK报红,使用下边的方法将顺丰jar上传到本地仓库。

上传成功,刷新pom.xml文件。
2) 表设计
出库单主表:存储出库单的基本信息
出库单明细表:存储出库商品的信息
出库分配明细表:存储出库商品的库存分配信息

3) 测试
阅读需求文档,测试创建出库单功能:
创建出库单
修改出库单
提交审核
审核出库单
3 分配库存
3.1 分析
1) 分配库存流程
2) 数据流
3)主业务方法
阅读下边的主业务方法,理解库存分配的流程。
库存分配接口:
@Tag(name="出库单")
@RestController
@RequestMapping("/outorder/wmsOutOrders")
@Slf4j
public Result<String> allocateStock(@RequestParam(name="ids",required=true) String ids) {
//非空校验
if (StringUtils.isEmpty(ids)) {
return Result.error("请选择需要分配库存的订单");
}
//分配成功数量
int allocatedCount = 0;
String[] idArray = ids.split(",");
for (String id : idArray) {
String i = WarehouseDictEnum.OUTBOUND_FAILED.getCode();
try {
//分配库存
i = wmsOutOrdersService.allocateStock(id);
} catch (Exception e) {
log.info("出库单{}分配库存失败", id);
}
if (i.equals(WarehouseDictEnum.OUTBOUND_ALLOCATED.getCode())) {
//成功数量加1
allocatedCount++;
}
}
//失败数量
int failNum = idArray.length - allocatedCount;
return Result.OK("分配库存成功" + allocatedCount + "条,失败" + failNum + "条");
}
...
}库存分配业务方法:
@Service
public class WmsOutOrdersServiceImpl extends ServiceImpl<WmsOutOrdersMapper, WmsOutOrders> implements IWmsOutOrdersService {
....
/**
* 分配库存
* @param orderId 出库单ID
* @return 分配状态 :"ALLOCATED"-分配成功 分配失败 "ALLOCATED_FAILED"-分配失败
*/
@Override
@Transactional(rollbackFor = Exception.class)
public String allocateStock(String orderId) {
//查询出库单
WmsOutOrders wmsOutOrders = wmsOutOrdersMapper.selectById(orderId);
//仓库id
String warehouseId = wmsOutOrders.getWarehouseId();
//出库单状态
String status = wmsOutOrders.getStatus();
//状态为审核通过状态或分配失败时才能继续
if(!(WarehouseDictEnum.OUTBOUND_APPROVED.getCode().equals(status))
&&!(WarehouseDictEnum.OUTBOUND_FAILED.getCode().equals(status))){
throw new JeecgBootException("非审核通过状态、分配失败状态出库单不允许分配库存");
}
//查询出库单明细
List<WmsOutOrdersItems> skuItems = wmsOutOrdersItemsService.selectByMainId(orderId);
//找到未分配的出库单明细(状态为创建状态为分配失败状态)
List<WmsOutOrdersItems> unAllocatedItems = skuItems.stream()
.filter(item -> WarehouseDictEnum.OUTBOUND_DETAIL_CREATED.getCode().equals(item.getStatus()) || WarehouseDictEnum.OUTBOUND_DETAIL_FAILED.getCode().equals(item.getStatus()))
.collect(Collectors.toList());
//如果未分配的出库单明细为空则返回已分配
if(CollectionUtils.isEmpty(unAllocatedItems)){
return WarehouseDictEnum.OUTBOUND_ALLOCATED.getCode();
}
//遍历未分配的出库单明细
for(WmsOutOrdersItems skuItem:unAllocatedItems) {
//todo 查询可用库存列表
//todo 进行库存分配
}
//todo更新出库单分配状态
}
....
}3.2 查询可用库存
1)service接口
public interface IWmsInventoryService extends IService<WmsInventory> {
.....
/**
* 查询可用库存
* @param warehouseId 仓库id
* @param skuItem sku信息
*/
public List<WmsInventory> selectAvailableBySku(String warehouseId, WmsOutOrdersItems skuItem);
}2) service接口实现
/**
* 查询可用库存
*/
public List<WmsInventory> selectAvailableBySku(String warehouseId, WmsOutOrdersItems skuItem){
/**
*"SELECT * FROM wms_inventory " +
* "WHERE is_sellable = '1' and product_id = #{skuId} " +
* "AND available_quantity > 0 " +
* "AND batch_number = #{batchNo} " +
* "AND warehouse_id = #{warehouseId} " +
* "ORDER BY expiry_date ASC, stock_in_time ASC")
*/
//使用MybatisPlus的查询条件
LambdaQueryWrapper<WmsInventory> queryWrapper = new LambdaQueryWrapper<>();
//可售库存
queryWrapper.eq(WmsInventory::getIsSellable, "1");
queryWrapper.eq(WmsInventory::getProductId, skuItem.getSkuId());
//如果有批次号是针对某批次的商品进行出库
queryWrapper.eq(StringUtils.isNotEmpty( skuItem.getBatchNumber()),WmsInventory::getBatchNumber, skuItem.getBatchNumber());
queryWrapper.gt(WmsInventory::getAvailableQuantity, 0);
//仓库id
queryWrapper.eq(WmsInventory::getWarehouseId, warehouseId);
//保质期到期日升序,入库时间升序
queryWrapper.orderByAsc(WmsInventory::getExpiryDate).orderByAsc(WmsInventory::getStockInTime);
return baseMapper.selectList(queryWrapper);
}3)完善主业务方法
定义好“查询可用库存”方法后完善主service方法。
/**
* 分配库存
* @param orderId 出库单ID
* @return 分配状态 :"ALLOCATED"-分配成功 分配失败 "ALLOCATED_FAILED"-分配失败
*/
@Override
@Transactional(rollbackFor = Exception.class)
@Override
@Transactional(rollbackFor = Exception.class)
public String allocateStock(String orderId) {
//查询出库单
WmsOutOrders wmsOutOrders = wmsOutOrdersMapper.selectById(orderId);
//仓库id
String warehouseId = wmsOutOrders.getWarehouseId();
//出库单状态
String status = wmsOutOrders.getStatus();
//状态为审核通过状态或分配失败时才能继续
if(!(WarehouseDictEnum.OUTBOUND_APPROVED.getCode().equals(status))
&&!(WarehouseDictEnum.OUTBOUND_FAILED.getCode().equals(status))){
throw new JeecgBootException("非审核通过状态、分配失败状态出库单不允许分配库存");
}
//查询出库单明细
List<WmsOutOrdersItems> skuItems = wmsOutOrdersItemsService.selectByMainId(orderId);
//找到未分配的出库单明细(状态为创建或状态为分配失败)
List<WmsOutOrdersItems> unAllocatedItems = skuItems.stream()
.filter(item -> WarehouseDictEnum.OUTBOUND_DETAIL_CREATED.getCode().equals(item.getStatus()) || WarehouseDictEnum.OUTBOUND_DETAIL_FAILED.getCode().equals(item.getStatus()))
.collect(Collectors.toList());
if(CollectionUtils.isEmpty(unAllocatedItems)){
return WarehouseDictEnum.OUTBOUND_FAILED.getCode();
}
//遍历未分配的出库单明细
for(WmsOutOrdersItems skuItem:unAllocatedItems) {
//查询可用库存列表
List<WmsInventory> wmsInventories = wmsInventoryService.selectAvailableBySku(warehouseId, skuItem);
//todo 进行库存分配
}
//todo 更新出库单分配状态
}3.3 分配库存
根据流程图可知,为商品分配库存需要完成以下事情:
- 遍历可用库存
- 添加库存分配记录
- 更新出库单明细的分配数量
- 存储库存表(锁定库存)
1)service接口
定义分配库存service接口
public interface IWmsOutOrdersService extends IService<WmsOutOrders> {
....
/**
* 为商品分配库存
* @param item 需分配的出库明细
* @param availableStocks 可用库存明细
* @return 分配状态
*/
public String allocateStockToItems(WmsOutOrdersItems item,
List<WmsInventory> availableStocks);
}2)service接口实现
/**
* 为商品分配库存
* @param item 需分配的出库明细
* @param availableStocks 可用库存明细
* @return 分配状态
*/
@Transactional(rollbackFor = Exception.class)
public String allocateStockToItems(WmsOutOrdersItems item, List<WmsInventory> availableStocks) {
//遍历可用库存
for(WmsInventory inventory:availableStocks){
//商品计划分配数量
Integer expectedQuantity = item.getExpectedQuantity();
//已分配数量
Integer allocatedQuantity = item.getAllocatedQuantity();
//剩余分配数量
Integer remainingQuantity = expectedQuantity - allocatedQuantity;
//如果分配完成,退出循环
if(remainingQuantity<=0){
break;
}
//可用库存数量
Integer availableQuantity = inventory.getAvailableQuantity();
if(availableQuantity<=0){
continue;
}
//本次要分配数量
Integer allocateQuantity = Math.min(availableQuantity, remainingQuantity);
//向库存分配表插入记录
WmsOutOrdersAllocation wmsOutOrdersAllocation = new WmsOutOrdersAllocation();
wmsOutOrdersAllocation.setOrderId(item.getOrderId());
wmsOutOrdersAllocation.setOrderItemId(item.getId());
wmsOutOrdersAllocation.setSkuId(item.getSkuId());
wmsOutOrdersAllocation.setLocationCode(inventory.getLocationCode());
wmsOutOrdersAllocation.setBatchNumber(inventory.getBatchNumber());
wmsOutOrdersAllocation.setContainerCode(inventory.getContainerCode());
wmsOutOrdersAllocation.setAllocatedQuantity(allocateQuantity);
wmsOutOrdersAllocation.setInventoryId(inventory.getId());
outOrdersAllocationService.save(wmsOutOrdersAllocation);
//锁定库存
WmsInventoryTransParam wmsInventoryTransParam = new WmsInventoryTransParam();
wmsInventoryTransParam.setProductId(item.getSkuId());
wmsInventoryTransParam.setExecQuantity(allocateQuantity);
wmsInventoryTransParam.setSourceLocationCode(inventory.getLocationCode());
wmsInventoryTransParam.setWarehouseId(inventory.getWarehouseId());
wmsInventoryTransParam.setBatchNumber(inventory.getBatchNumber());
wmsInventoryTransParam.setTransactionType(WarehouseDictEnum.INVENTORY_ALLOCATION.getCode());
wmsInventoryTransParam.setOperationTime(new Date());
wmsInventoryTransByAllocation.transfer(wmsInventoryTransParam);
//更新出库单明细中分配数量
wmsOutOrdersItemsService.allocateStock(item.getId(), allocateQuantity);
//查询最新的item
item = wmsOutOrdersItemsService.getById(item.getId());
}
//更新出库单明细的状态(已分配或分配失败)
String allocateStatus = wmsOutOrdersItemsService.updateAllocateStatus(item.getId());
return allocateStatus;
}3) 完善主业务方法
定义好“为商品分配库存”方法后完善主service方法。
/**
* 分配库存
* @param orderId 出库单ID
* @return 分配状态 :"ALLOCATED"-分配成功 分配失败 "ALLOCATED_FAILED"-分配失败
*/
@Override
@Transactional(rollbackFor = Exception.class)
public String allocateStock(String orderId) {
//查询出库单
WmsOutOrders wmsOutOrders = wmsOutOrdersMapper.selectById(orderId);
//仓库id
String warehouseId = wmsOutOrders.getWarehouseId();
//出库单状态
String status = wmsOutOrders.getStatus();
//状态为审核通过状态或分配失败时才能继续
if(!(WarehouseDictEnum.OUTBOUND_APPROVED.getCode().equals(status))
&&!(WarehouseDictEnum.OUTBOUND_FAILED.getCode().equals(status))){
throw new JeecgBootException("非审核通过状态、分配失败状态出库单不允许分配库存");
}
//查询出库单明细
List<WmsOutOrdersItems> skuItems = wmsOutOrdersItemsService.selectByMainId(orderId);
//找到未分配的出库单明细(状态为创建或状态为分配失败)
List<WmsOutOrdersItems> unAllocatedItems = skuItems.stream()
.filter(item -> WarehouseDictEnum.OUTBOUND_DETAIL_CREATED.getCode().equals(item.getStatus()) || WarehouseDictEnum.OUTBOUND_DETAIL_FAILED.getCode().equals(item.getStatus()))
.collect(Collectors.toList());
if(CollectionUtils.isEmpty(unAllocatedItems)){
return WarehouseDictEnum.OUTBOUND_FAILED.getCode();
}
//遍历未分配的出库单明细
for(WmsOutOrdersItems skuItem:unAllocatedItems) {
//查询可用库存列表
List<WmsInventory> wmsInventories = wmsInventoryService.selectAvailableBySku(warehouseId, skuItem);
//进行库存分配
String s = allocateStockToItems(skuItem, wmsInventories);
//如果分配失败则抛出异常
if(!s.equals(WarehouseDictEnum.OUTBOUND_DETAIL_ALLOCATED.getCode())){
throw new JeecgBootException("分配库存失败");
}
}
//更新出库单分配状态
String allocateStatus = updateAllocateStatus(orderId);
return allocateStatus;
}3.4 测试分配库存
正常情况:
- 为一个商品分配库存
- 为多个商品分配库存
- 模拟从多个库存为商品分配库存
异常情况:
- 库存不足的情况
4 创建波次
4.1 需求分析
常见的波次策略
- 单品单件波次策略
含义:每个波次只包含一种商品,且每个订单中该商品的数量为1。
适用场景:适用于订单中商品种类多但每种商品数量少的情况。
优点:拣货路径简单,效率高。
缺点:订单量较大时,波次数量多,管理复杂。
- 一品2件、一品3件
含义:
- 一品2件:每个波次包含一种商品,每个订单中该商品的数量为2。
- 一品3件:每个波次包含一种商品,每个订单中该商品的数量为3。
适用场景:适用于订单中某些商品需求量为2或3的情况。
优点:减少拣货次数,提高效率。
缺点:商品需求量不固定时,适用性有限。
- 一品波次策略
含义:每个波次只包含一种商品,但每个订单中该商品的数量不限。
适用场景:适用于订单中某些商品需求量较大的情况。
优点:集中处理单一商品,提高拣货效率。
缺点:订单中商品种类多时,波次数量多,管理复杂。
- (A+B)2件
含义:每个波次包含两种商品(A和B),每个订单中这两种商品的总数量为2。
适用场景:适用于订单中常同时出现两种商品且总需求量为2的情况。
优点:减少拣货次数,提高效率。
缺点:商品组合不固定时,适用性有限。
- 一单一件多品波次
含义:每个波次包含多个订单,每个订单中只包含一种商品,且数量为1。
适用场景:适用于订单中商品种类多但每种商品数量少的情况。
优点:减少波次数量,提高拣货效率。
缺点:订单中商品种类多时,拣货路径复杂。
- 单区波次
含义:每个波次只包含一个仓库区域内的订单。
适用场景:适用于仓库分区明确且订单集中在某个区域的情况。
优点:减少拣货路径,提高效率。
缺点:订单分布不均时,适用性有限。
- 混合-默认策略
含义:结合多种波次策略,根据实际情况动态调整。
适用场景:适用于订单种类和数量变化较大的情况。
优点:灵活性强,适应各种情况。
缺点:管理复杂,需要系统支持。
- 尾单策略
含义:尾单策略将未及时处理的订单归入一个波次,通常是在特定时间段结束时执行。例如,每天下午5点将当天未处理的订单集中处理。
适用场景
- 固定发货时间:如电商仓库每天固定时间发货。
- 订单量波动大:订单量不稳定时,尾单策略能有效处理积压订单
优点
- 减少遗漏:确保所有订单按时处理。
- 优化资源:集中处理订单,提升资源利用率。
缺点
- 延迟风险:订单可能延迟到波次执行时才处理。
- 灵活性不足:不适合需要实时处理的场景。
举例
创建波次流程:
找出当前没有划分波次的出库单,按波次策略优先级依次去匹配。
先拿单品单件波次策略去匹配每个出库单,凡是匹配成功的被划分为一个波次。
匹配完第一个波次策略后,如果还剩余出库单未匹配则继续拿下一个波次策略去匹配,依次类推,直到匹配完成所有选择的波次策略。
出库单如下:
编号:OBD001、仓库:中原1号仓库、货主:北京传智教育
出库单明细:
商品编码:C01、出库数量:100件
商品编码:C02、出库数量:50件、批号:202201
编号:OBD002、仓库:中原1号仓库、货主:北京传智教育
出库单明细:
商品编码:C01、出库数量:60件
编号:OBD003、仓库:中原1号仓库、货主:北京传智教育
出库单明细:
商品编码:C01、出库数量:1件1
编号:OBD004、仓库:中原1号仓库、货主:北京传智教育
出库单明细:
商品编码:C01、出库数量:1件
选择的波次策略:单品单件、尾单策略。
首先用单品单件波次策略匹配所有订单,OBD003和OBD004符合单品单件,所有OBD003和OBD004划分为一个波次。
再将剩余订单匹配尾单策略,出库单OBD001和OBD002符合尾单策略。
最终创建两个波次。
B001:尾单策略,包括OBD001、OBD002出库单
B002:单品策略,包括OBD003、OBD004出库单
4.2 分析设计
1)表结构
波次表:存储波次的信息
波次策略表:存储波次策略信息
波次与出库单的关系:一对多关系,实现方法:在出库单主表添加波次ID字段。

2)执行流程
3)数据流
4.3 设计模式(编写测试代码)
在创建波次的流程中需要根据波次策略对出库单进行分组,为了提高系统的可扩展性,我们在开发波次策略类时需要用到设计模式,另外在在按优先级匹配每个波次策略时也会用到设计模式。
4.3.1 策略链模式
1)什么是设计模式?
设计模式是软件设计中常见问题的典型、可重用的解决方案模板。 它不是什么可以直接转换成代码的库或框架,而是一种高级的设计思想,用来指导你写出更优雅、更健壮、更易维护的代码。
想象一下你要盖房子。
- 第一次盖房: 你可能毫无头绪,不知道厨房、卧室、卫生间该怎么布局才合理。你可能会把卫生间设计在厨房隔壁,导致水管绕远,或者卧室采光不好。
- 建筑师的经验: 有经验的建筑师知道,经过长期实践,某些布局方式是最优的。例如,“南北通透”是一种经典布局模式,能保证良好的采光和通风。
在这里,“南北通透”就是一种“设计模式”。
为什么需要设计模式?(解决的问题)
在没有设计模式的情况下,软件开发常常会遇到以下问题:
- 代码冗余: 同样的问题,不同的程序员会用不同的、且可能很笨拙的方式解决一遍又一遍。
- 难以维护: 代码结构混乱,牵一发而动全身,修改一个功能可能导致意想不到的bug。
- 难以沟通: 开发者之间讨论设计时,需要花费大量口舌来描述一个复杂的解决方案。
- 难以扩展: 添加新功能变得异常困难,因为代码的各部分耦合得太紧密。
设计模式的出现,就是为了标准化这些优秀解决方案,让开发者可以:
- 重用成功的设计和架构。
- 提高代码的可读性和可维护性。
- 方便开发者之间的交流。你说“这里我们用个单例模式”,大家立刻就明白了你的意图,无需过多解释。
- 提高代码的灵活性和可扩展性。
设计模式三大类
- 创建型模式
关注点: 对象创建的机制。
目标: 以灵活、可控制的方式创建对象,而不是直接使用
new关键字。常见模式:
- 单例模式: 保证一个类只有一个实例。
- 工厂方法模式: 定义一个创建对象的接口,但让子类决定实例化哪个类。
- 抽象工厂模式: 创建相关或依赖对象的家族,而不需要明确指定具体类。
- 结构型模式
关注点: 如何将类或对象组合成更大的结构。
目标: 通过继承或组合,简化不同部分之间的关系,保持结构的灵活和高效。
常见模式:
- 适配器模式: 让不兼容的接口可以一起工作(像电源转换插头)。
- 装饰器模式: 动态地给一个对象添加额外的功能(像给游戏角色穿装备)。
- 代理模式: 为其他对象提供一个代理以控制对这个对象的访问。
- 行为型模式
关注点: 对象之间的职责分配和通信。
目标: 更好地实现对象间的交互和算法的流转。
常见模式:
- 观察者模式: 定义一种一对多的依赖关系,当一个对象状态改变时,所有依赖它的对象都会得到通知(如事件处理系统)。
- 策略模式: 定义一系列算法,将它们封装起来,并且使它们可以互相替换。
- 模板方法模式: 定义一个操作中的算法骨架,而将一些步骤延迟到子类中。
2) 举例
责任链模式是一种行为设计模式,它允许你将请求沿着处理链传递,直到有一个处理者能够处理它为止。
示例场景
假设我们有一个审批系统,不同级别的管理者有不同的审批权限:
- 项目经理(可审批金额 ≤ 1000)
- 部门经理(可审批金额 ≤ 5000)
- 总经理(可审批金额 > 5000)
代码实现
解压“课程资料/源码/后端/设计模式示例.zip”
将“designmode”目录下的类拷贝到org.jeecg.modules.wms.designmode下。
将“designmode2”目录下的类拷贝到org.jeecg.modules.wms.designmode2下。
首先定义抽象处理者
package org.jeecg.modules.wms.designmode;
// 抽象处理者
public abstract class Approver {
protected Approver nextApprover; // 下一个处理者
// 设置下一个处理者
public void setNextApprover(Approver nextApprover) {
this.nextApprover = nextApprover;
}
// 处理请求的方法
public abstract void processRequest(PurchaseRequest request);
}定义请求类
package org.jeecg.modules.wms.designmode;
import lombok.Data;
// 请求类
@Data
public class PurchaseRequest {
private int id;
private double amount;
private String purpose;
public PurchaseRequest(int id, double amount, String purpose) {
this.id = id;
this.amount = amount;
this.purpose = purpose;
}
}实现具体处理者
package org.jeecg.modules.wms.designmode;
// 项目经理
public class ProjectManager extends Approver {
@Override
public void processRequest(PurchaseRequest request) {
System.out.println("项目经理审批采购单:" + request.getId() +
",金额:" + request.getAmount() +
",用途:" + request.getPurpose());
if (nextApprover != null) {
nextApprover.processRequest(request);
}
}
}
// 部门经理
public class DepartmentManager extends Approver {
@Override
public void processRequest(PurchaseRequest request) {
if (request.getAmount() > 1000 ) {
System.out.println("部门经理审批采购单:" + request.getId() +
",金额:" + request.getAmount() +
",用途:" + request.getPurpose());
}
if (nextApprover != null) {
nextApprover.processRequest(request);
}
}
}
// 总经理
public class GeneralManager extends Approver {
@Override
public void processRequest(PurchaseRequest request) {
if(request.getAmount() > 5000){
System.out.println("总经理审批采购单:" + request.getId() +
",金额:" + request.getAmount() +
",用途:" + request.getPurpose());
}
}
}- 客户端使用
public class Client {
public static void main(String[] args) {
// 创建处理者
Approver projectManager = new ProjectManager();
Approver departmentManager = new DepartmentManager();
Approver generalManager = new GeneralManager();
// 设置责任链
projectManager.setNextApprover(departmentManager);
departmentManager.setNextApprover(generalManager);
// 创建请求
PurchaseRequest request1 = new PurchaseRequest(1, 800, "购买办公用品");
PurchaseRequest request2 = new PurchaseRequest(2, 3500, "购买服务器");
PurchaseRequest request3 = new PurchaseRequest(3, 10000, "购买新设备");
// 处理请求
// projectManager.processRequest(request1);
// projectManager.processRequest(request2);
projectManager.processRequest(request3);
}
}输出结果
项目经理审批采购单:3,金额:10000.0,用途:购买新设备
部门经理审批采购单:3,金额:10000.0,用途:购买新设备
总经理审批采购单:3,金额:10000.0,用途:购买新设备3)总结
责任链模式为请求创建了一个接收者对象的链。使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
角色构成
- 抽象处理器: 定义了一个处理请求的接口,通常包含一个指向下一个处理器的引用(后继者)。
- 具体处理器: 实现了抽象处理器的接口。如果可以处理请求,就处理它;否则,将请求转发给它的后继者。
- 客户端: 创建处理链,并向链头的具体处理器对象提交请求。
责任链模式的特点
- 解耦:发送者和接收者之间解耦,发送者不需要知道具体由哪个接收者处理请求
- 灵活性:可以动态地改变链中的成员或调整它们的顺序
- 可扩展性:可以方便地新增处理者
缺点: 请求不一定能被处理(如果链配置不当);调试可能不太直观。
应用场景:
- 有多个对象可以处理同一个请求,但具体由哪个对象处理则在运行时动态决定。
- 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
- 可动态指定一组对象处理请求。例如:审批流程、异常处理、过滤器链(Servlet Filter)。
4.3.2 策略模式
1)举例
示例:
下边阅读org.jeecg.modules.wms.designmode2包下的代码
假设我们有一个订单处理系统,需要根据不同条件应用不同的折扣策略:
package org.jeecg.modules.wms.designmode2;
import lombok.Data;
// 订单类
@Data
class Order {
private double amount;
private boolean isMember;
private boolean isHoliday;
private boolean isFirstOrder;
public Order(double amount, boolean isMember, boolean isHoliday, boolean isFirstOrder) {
this.amount = amount;
this.isMember = isMember;
this.isHoliday = isHoliday;
this.isFirstOrder = isFirstOrder;
}
}折扣策略接口
package org.jeecg.modules.wms.designmode2;
// 折扣策略接口
interface DiscountStrategy {
boolean apply(Order order);
}package org.jeecg.modules.wms.designmode2;
// 会员折扣策略
class MemberDiscount implements DiscountStrategy {
@Override
public boolean apply(Order order) {
if (order.isMember()) {
double newAmount = order.getAmount() * 0.9; // 会员9折
order.setAmount(newAmount);
System.out.println("应用会员折扣,新金额: " + newAmount);
return true;
}
return false;
}
}
// 节日折扣策略
class HolidayDiscount implements DiscountStrategy {
@Override
public boolean apply(Order order) {
if (order.isHoliday()) {
double newAmount = order.getAmount() * 0.85; // 节日85折
order.setAmount(newAmount);
System.out.println("应用节日折扣,新金额: " + newAmount);
return true;
}
return false;
}
}
// 首单折扣策略
class FirstOrderDiscount implements DiscountStrategy {
@Override
public boolean apply(Order order) {
if (order.isFirstOrder()) {
double newAmount = order.getAmount() * 0.8; // 首单8折
order.setAmount(newAmount);
System.out.println("应用首单折扣,新金额: " + newAmount);
return true;
}
return false;
}
}
// 大额订单折扣策略
class BigOrderDiscount implements DiscountStrategy {
@Override
public boolean apply(Order order) {
if (order.getAmount() > 1000) {
double newAmount = order.getAmount() * 0.7; // 大额7折
order.setAmount(newAmount);
System.out.println("应用大额折扣,新金额: " + newAmount);
return true;
}
return false;
}
}折扣策略链
package org.jeecg.modules.wms.designmode2;
import java.util.ArrayList;
import java.util.List;
// 折扣策略链
class DiscountStrategyChain {
private List<DiscountStrategy> strategies = new ArrayList<>();
public DiscountStrategyChain addStrategy(DiscountStrategy strategy) {
strategies.add(strategy);
return this;
}
public void applyDiscounts(Order order) {
for (DiscountStrategy strategy : strategies) {
strategy.apply(order);
}
}
}客户端类
package org.jeecg.modules.wms.designmode2;
// 客户端代码
public class OrderProcessingDemo {
public static void main(String[] args) {
// 创建策略链
DiscountStrategyChain chain = new DiscountStrategyChain();
chain.addStrategy(new MemberDiscount())
.addStrategy(new HolidayDiscount())
.addStrategy(new FirstOrderDiscount())
.addStrategy(new BigOrderDiscount());
// 处理订单1: 会员 + 节日
System.out.println("\n处理订单1:");
Order order1 = new Order(500, true, true, false);
chain.applyDiscounts(order1);
System.out.println("最终金额: " + order1.getAmount());
// 处理订单2: 首单 + 大额
System.out.println("\n处理订单2:");
Order order2 = new Order(1500, false, false, true);
chain.applyDiscounts(order2);
System.out.println("最终金额: " + order2.getAmount());
// 处理订单3: 无折扣
System.out.println("\n处理订单3:");
Order order3 = new Order(200, false, false, false);
chain.applyDiscounts(order3);
System.out.println("最终金额: " + order3.getAmount());
}
}运行结果:
处理订单1:
应用会员折扣,新金额: 450.0
应用节日折扣,新金额: 382.5
最终金额: 382.5
处理订单2:
应用首单折扣,新金额: 1200.0
应用大额折扣,新金额: 840.0
最终金额: 840.0
处理订单3:
最终金额: 200.02)总结
策略模式定义了一系列的算法,并将每一个算法封装起来,使它们可以相互替换。策略模式让算法的变化独立于使用算法的客户端。
策略模式通常包含三个角色:
- 抽象策略: 通常是一个接口或抽象类。它定义了所有具体策略必须实现的方法,也就是技能的公共接口。
- 具体策略: 实现了抽象策略接口的具体算法类。
- 环境类: 持有一个策略类的引用。它负责对接客户端,接收具体的策略实例,并执行策略。
策略模式的特点
- 灵活性:可以动态添加、删除或重新排序策略
- 可扩展性:新策略可以很容易地添加到现有系统中
- 解耦:每个策略只关注自己的处理逻辑
- 可组合性:多个策略可以组合使用
应用场景:
- 一个系统需要动态地在几种算法中选择一种。
- 有多个条件语句的、行为相似的类。
- 希望将算法的实现细节对客户端隐藏。
4.4 定义波次策略
1)策略抽象类
package org.jeecg.modules.wms.wave.strategy;
import org.jeecg.modules.wms.outorder.entity.WmsOutOrders;
import org.jeecg.modules.wms.outorder.entity.WmsOutOrdersAllocation;
import java.util.List;
import java.util.Map;
/**
* 波次策略接口
*/
public abstract class IWaveStrategy {
/**
* 下一个策略
*/
private IWaveStrategy nextStrategy;
public void setNextStrategy(IWaveStrategy nextStrategy) {
this.nextStrategy = nextStrategy;
}
/**
* 创建波次
* @param orders 待处理的订单列表
* @param allocationsMap 订单分配明细映射
*/
abstract void process(List<WmsOutOrders> orders,
Map<String, List<WmsOutOrdersAllocation>> allocationsMap);
/**
* 获取策略类型
*/
abstract String getStrategyType();
/**
* 获取策略优先级(数值越小优先级越高)
*/
abstract int getPriority();
/**
* 下一个策略
*/
public IWaveStrategy next(){
return nextStrategy;
}
/**
* 执行下一个策略
*/
public void processNext(List<WmsOutOrders> orders, Map<String, List<WmsOutOrdersAllocation>> allocationsMap) {
if (orders.size() > 0 && next() != null) {
next().process(orders, allocationsMap);
}
}
}2)策略类
单品单件波次策略
package org.jeecg.modules.wms.wave.strategy;
import lombok.RequiredArgsConstructor;
import org.jeecg.modules.wms.outorder.entity.WmsOutOrders;
import org.jeecg.modules.wms.outorder.entity.WmsOutOrdersAllocation;
import org.jeecg.modules.wms.outorder.entity.WmsOutOrdersItems;
import org.jeecg.modules.wms.outorder.service.IWmsOutOrdersItemsService;
import org.jeecg.modules.wms.wave.entity.WmsWaveStrategy;
import org.jeecg.modules.wms.wave.service.IWaveStrategyService;
import org.jeecg.modules.wms.wave.service.IWmsWaveMasterService;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 单品单件波次策略 - SISQ
* 条件:订单中只有一个SKU,且总数量为1
*/
@Component
@RequiredArgsConstructor
public class SISQWaveStrategy extends IWaveStrategy {
private final IWmsWaveMasterService wmsWaveMasterService;
private final IWaveStrategyService waveStrategyService;
private final IWmsOutOrdersItemsService wmsOutOrdersItemsService;
@Override
public void process(List<WmsOutOrders> orders,
Map<String, List<WmsOutOrdersAllocation>> allocationsMap) {
//包裹策略为STANDARD_STRATEGY
String shipmentStrategy = "STANDARD_STRATEGY";
//找到符合单品单件的订单
List<WmsOutOrders> matchedOrders = orders.stream()
.filter(order -> order.getTotalSku() == 1 && order.getTotalQuantity() == 1)
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(matchedOrders)) {
processNext(orders, allocationsMap);
return;
}
//从数据库取出当前策略的详细信息
WmsWaveStrategy strategyByCode = waveStrategyService.getStrategyByCode(this.getStrategyType());
//最小订单数
int minOrderCount = strategyByCode.getMinOrderCount();
//出库单id
List<String> orderIds = matchedOrders.stream().map(WmsOutOrders::getId).collect(Collectors.toList());
//根据出库单id查询出库单明细
List<WmsOutOrdersItems> allocations = wmsOutOrdersItemsService.selectByOrderIds(orderIds);
//遍历matchedOrders,从allocations中找到对应的出库单明细并找到skuId,赋值到matchedOrders的对象中
for (WmsOutOrders order : matchedOrders) {
WmsOutOrdersItems allocation = allocations.stream()
.filter(item -> item.getOrderId().equals(order.getId()))
.findFirst()
.orElse(new WmsOutOrdersItems());
order.setSkuId(allocation.getSkuId());
}
//对matchedOrders按skuId分组
Map<String, List<WmsOutOrders>> groupedOrders = matchedOrders.stream().collect(Collectors.groupingBy(WmsOutOrders::getSkuId));
//添加到波次的订单
List<WmsOutOrders> wmsOutOrders = new ArrayList<>();
//遍历groupedOrders
for (Map.Entry<String, List<WmsOutOrders>> entry : groupedOrders.entrySet()) {
List<WmsOutOrders> skuOrders = entry.getValue();
if (skuOrders.size() >= minOrderCount) {
wmsWaveMasterService.addWave(skuOrders, getStrategyType(), shipmentStrategy);
wmsOutOrders.addAll(skuOrders);
}
}
//剩余的订单
List<WmsOutOrders> collect = orders.stream()
.filter(order -> !wmsOutOrders.contains(order))
.collect(Collectors.toList());
//执行下一个
processNext(collect, allocationsMap);
}
@Override
public String getStrategyType() {
return "SISQ";
}
@Override
public int getPriority() {
return 1;
}
}其它策略
从课程资料拷贝“源代码/后端/波次策略”到org.jeecg.modules.wms.wave.strategy包下

拷贝课程资料下“源代码/后端/config/WaveStrategyConfig.java”到org.jeecg.modules.wms.config
4.6 创建波次接口
本接口由用户端调用,收到用户请求通过波次策略创建波次。
1)controller(阅读)
@Tag(name="波次主表")
@RestController
@RequestMapping("/wave/wmsWaveMaster")
@Slf4j
public class WmsWaveMasterController extends JeecgController<WmsWaveMaster, IWmsWaveMasterService> {
....
@AutoLog(value = "波次主表-创建波次")
@Operation(summary="波次主表-创建波次")
@PostMapping(value = "/createWave")
public Result<String> createWave(@RequestBody WmsWaveAdd wmsWaveAdd) {
//获取波次策略
String strategyCodes = wmsWaveAdd.getStrategyCodes();
//仓库
String warehouseId = wmsWaveAdd.getWarehouseId();
if (oConvertUtils.isEmpty(strategyCodes)) {
return Result.error("请选择波次策略!");
}
String[] strategyCodesArr = strategyCodes.split(",");
//创建波次
wmsWaveMasterService.createWave(warehouseId,Arrays.asList(strategyCodesArr));
return Result.OK("创建成功!");
}
...
}2) service(阅读)
@Service
public class WmsWaveMasterServiceImpl extends ServiceImpl<WmsWaveMasterMapper, WmsWaveMaster> implements IWmsWaveMasterService {
......
//波次处理策略
@Autowired
private List<IWaveStrategy> strategies;
@Override
@Transactional
public void createWave(String warehouseId,List<String> strategyCodes) {
// 1. 获取已分配库存且未创建波次的出库单
List<WmsOutOrders> unassignedOrders = wmsOutOrdersService.selectUnassignedOrders(warehouseId);
if (unassignedOrders.isEmpty()) {
return;
}
// 2. 获取这些出库单的分配明细
List<String> orderIds = unassignedOrders.stream()
.map(WmsOutOrders::getId)
.collect(Collectors.toList());
Map<String, List<WmsOutOrdersAllocation>> allocationsMap = wmsOutOrdersAllocationMapper.selectAllocationsByOrderIds(orderIds)
.stream()
.collect(Collectors.groupingBy(WmsOutOrdersAllocation::getOrderId));
// 3. todo 使用责任链处理波次创建
}
......
}3)客户端类(编写)
客户端类是调用波次策略的入口,它的功能是组织波次策略,并提供调用波次策略的方法。
package org.jeecg.modules.wms.wave.strategy;
import org.jeecg.modules.wms.outorder.entity.WmsOutOrders;
import org.jeecg.modules.wms.outorder.entity.WmsOutOrdersAllocation;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 创建波次客户端类
*/
public class WaveCreateClient {
private IWaveStrategy firstStrategy;
/**
* 构造方法
*
* @param strategies 全部波次策略
* @param selectedStrategies 选中的波次策略编码
*/
public WaveCreateClient(List<IWaveStrategy> strategies, List<String> selectedStrategies) {
// 从全部策略中,获取选中的策略,按优先级排序策略
List<IWaveStrategy> filteredStrategies = strategies.stream()
.filter(strategy -> selectedStrategies.contains(strategy.getStrategyType()))
.sorted((s1, s2) -> Integer.compare(s1.getPriority(), s2.getPriority()))
.collect(Collectors.toList());
if(CollectionUtils.isEmpty( filteredStrategies )){
return;
}
//将filteredStrategies中的策略对象组成责任链
//获取第一个策略
firstStrategy = filteredStrategies.get(0);
//将剩余的策略设置成第一个策略的nextStrategy
for (int i = 1; i < filteredStrategies.size(); i++) {
filteredStrategies.get(i - 1).setNextStrategy(filteredStrategies.get(i));
}
}
/**
* 处理订单波次分配
*/
public void process(List<WmsOutOrders> orders,
Map<String, List<WmsOutOrdersAllocation>> allocationsMap) {
if(firstStrategy == null){
throw new JeecgBootException("没有配置波次策略");
}
firstStrategy.process(orders, allocationsMap);
}
}完善创建波次service代码:
@Service
public class WmsWaveMasterServiceImpl extends ServiceImpl<WmsWaveMasterMapper, WmsWaveMaster> implements IWmsWaveMasterService {
......
//波次处理策略
@Autowired
private List<IWaveStrategy> strategies;
@Override
@Transactional
public void createWave(String warehouseId,List<String> strategyCodes) {
// 1. 获取已分配库存且未创建波次的出库单
List<WmsOutOrders> unassignedOrders = wmsOutOrdersService.selectUnassignedOrders(warehouseId);
if (unassignedOrders.isEmpty()) {
return;
}
// 2. 获取这些出库单的分配明细
List<String> orderIds = unassignedOrders.stream()
.map(WmsOutOrders::getId)
.collect(Collectors.toList());
Map<String, List<WmsOutOrdersAllocation>> allocationsMap = wmsOutOrdersAllocationMapper.selectAllocationsByOrderIds(orderIds)
.stream()
.collect(Collectors.groupingBy(WmsOutOrdersAllocation::getOrderId));
// 3. 使用责任链处理波次创建
WaveCreateClient waveCreateClient = new WaveCreateClient(strategies,strategyCodes);
waveCreateClient.process(unassignedOrders, allocationsMap);
}
......
}4) 测试
- 创建出库单并分配库存
- 创建波次,选择相应的波次策略
- 观察出库单信息是否关联波次
5 创建拣货任务
5.1 分析
1)创建思路
根据波次找到波次下边所有出库单的库存分配信息
对库存分配信息进行分类统计(根据商品id、储位、批号),统计结果就是拣货明细
将拣货明细插入到波次拣货明细表
根据波次拣货明细创建拣货任务
通过路径规划算法将拣货明细规划一个最优路径,工人根据规划的路径去拣货
2)表结构
波次拣货明细表、任务表:

3) 执行流程
4)数据流
5.2 统计库存分配
根据创建拣货任务的流程,需要根据波次统计波次下出库单的库存分配,按skuId、储位、批号分类汇总。
最终统计的结果会插入到波次拣货明细表。
1)sql
select a.*, b.product_barcode, b.owner_id
from (select t.sku_id,
t.location_code,
t.batch_number,
sum(t.allocated_quantity) allocated_quantity
from wms_out_orders_allocation t
where t.order_id in (select id
from wms_out_orders
where wave_id = ?)
group by t.sku_id, t.location_code, t.batch_number) a
left join wms_products b on a.sku_id = b.id2)mapper
mapper映射:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.jeecg.modules.wms.outorder.mapper.WmsOutOrdersAllocationMapper">
<select id="selectAllocatedQuantityByWaveId"
resultType="org.jeecg.modules.wms.outorder.entity.WmsOutOrdersAllocation"
parameterType="java.lang.String">
select a.*, b.product_barcode, b.owner_id
from (select t.sku_id,
t.location_code,
t.batch_number,
sum(t.allocated_quantity) allocated_quantity
from wms_out_orders_allocation t
where t.order_id in (select id
from wms_out_orders
where wave_id = #{value})
group by t.sku_id, t.location_code, t.batch_number) a
left join wms_products b on a.sku_id = b.id
</select>
...mapper接口:
/**
* @Description: 出库单分配明细
* @Author: jeecg-boot
* @Date: 2025-05-21
* @Version: V1.0
*/
public interface WmsOutOrdersAllocationMapper extends BaseMapper<WmsOutOrdersAllocation> {
/**
* 根据波次统计出库单的商品分配数量
*/
public List<WmsOutOrdersAllocation> selectAllocatedQuantityByWaveId(String waveId);
....3.) service
定义一个service接口分页查询库存分配的统计结果
service接口:
/**
* @Description: 出库单分配明细
* @Author: jeecg-boot
* @Date: 2025-05-21
* @Version: V1.0
*/
public interface IWmsOutOrdersAllocationService extends IService<WmsOutOrdersAllocation> {
/**
* 根据波次统计出库单的商品分配数量,分页查询结果
* @param waveId 波次id
* @param pageNo 页码
* @param pageSize 每页条数
* @return
*/
public IPage<WmsOutOrdersAllocation> selectAllocatedQuantityByWaveId(String waveId, Integer pageNo, Integer pageSize);
....接口实现类:
@Service
public class WmsOutOrdersAllocationServiceImpl extends ServiceImpl<WmsOutOrdersAllocationMapper, WmsOutOrdersAllocation> implements IWmsOutOrdersAllocationService {
/**
* 根据波次统计出库单的商品分配数量,分页查询结果
*/
public IPage<WmsOutOrdersAllocation> selectAllocatedQuantityByWaveId(String waveId, Integer pageNo, Integer pageSize) {
Page<WmsStockInOrderItems> page = PageHelper.startPage(pageNo, pageSize);
List<WmsOutOrdersAllocation> list = wmsOutOrdersAllocationMapper.selectAllocatedQuantityByWaveId(waveId);
PageDTO<WmsOutOrdersAllocation> wmsOutOrdersAllocationPageDTO = new PageDTO<>();
wmsOutOrdersAllocationPageDTO.setRecords(list);
wmsOutOrdersAllocationPageDTO.setTotal(page.getTotal());
wmsOutOrdersAllocationPageDTO.setSize(page.getPageSize());
wmsOutOrdersAllocationPageDTO.setCurrent(page.getPageNum());
wmsOutOrdersAllocationPageDTO.setPages(page.getPages());
return wmsOutOrdersAllocationPageDTO;
}
...5.3 创建拣货任务接口
1)controller
@Tag(name="拣货任务接口")
@RestController
@RequestMapping("/wave/pickingTasks")
@Slf4j
public class PickingTasksController {
.....
@AutoLog(value = "波次主表-创建拣货任务")
@Operation(summary="波次主表-创建拣货任务")
@PostMapping(value = "/createPickTask")
public Result<String> createPickTask(@RequestParam(name="ids",required=true) String ids) {
List<String> waveIds = Arrays.asList(ids.split(","));
for (String waveId : waveIds) {
createPickTask(waveId);
}
return Result.OK("创建成功!");
}
.....2) service
public interface IPickingTasksService {
/**
* 创建波次拣货明细
*/
void createPickTask(String waveId);
....接口实现:
@Service
public class PickingTasksServiceImpl implements IPickingTasksService {
.....
/**
* 生成拣货任务,根据波次统计出库单的商品分配数量,添加到波次拣货明细表
* @param waveId 波次id
* @return
*/
@Transactional(rollbackFor = Exception.class)
public void createPickTask(String waveId) {
//更新波次状态为拣货中
WmsWaveMaster wmsWaveMaster = wmsWaveMasterService.getById(waveId);
//将波次由创建状态改为拣货中
LambdaUpdateWrapper<WmsWaveMaster> set = new LambdaUpdateWrapper<WmsWaveMaster>()
.eq(WmsWaveMaster::getId, waveId)
.eq(WmsWaveMaster::getStatus, WarehouseDictEnum.WAVE_CREATED.getCode())
.set(WmsWaveMaster::getStatus, WarehouseDictEnum.WAVE_PICKING.getCode());
boolean update = wmsWaveMasterService.update(null, set);
if (!update) {
throw new JeecgBootException("更新波次状态为拣货中失败");
}
//根据波次id统计出库单的商品分配数量,分页查询
IPage<WmsOutOrdersAllocation> page = wmsOutOrdersAllocationService.selectAllocatedQuantityByWaveId(waveId, 1, 100);
if (page.getRecords().isEmpty()) {
throw new JeecgBootException("没有找到波次下的库存分配明细");
}
int n=1;
while (true) {
//获取记录
List<WmsOutOrdersAllocation> records = page.getRecords();
//将记录转换成波次拣货明细
List<WmsWaveSkuSummary> collect = records.stream().map(record -> {
WmsWaveSkuSummary wmsWaveSkuSummary = new WmsWaveSkuSummary();
wmsWaveSkuSummary.setWaveId(waveId);
wmsWaveSkuSummary.setSkuId(record.getSkuId());
wmsWaveSkuSummary.setOwnerId(record.getOwnerId());
wmsWaveSkuSummary.setProductBarcode(record.getProductBarcode());
wmsWaveSkuSummary.setLocationCode(record.getLocationCode());
wmsWaveSkuSummary.setBatchNumber(record.getBatchNumber());
wmsWaveSkuSummary.setAllocatedQuantity(record.getAllocatedQuantity());
wmsWaveSkuSummary.setPickedQuantity(0);
//默认状态 拣货中
wmsWaveSkuSummary.setStatus(WarehouseDictEnum.WAVESKU_PICKING.getCode());
return wmsWaveSkuSummary;
}).collect(Collectors.toList());
//批量插入波次拣货明细表
boolean b = wmsWaveSkuSummaryService.saveBatch(collect);
if (!b) {
throw new JeecgBootException("批量插入波次拣货明细表失败");
}
//根据拣货明细向任务表插入分拣任务
List<WmsTasks> wmsTaskList = collect.stream().map(wmsWaveSkuSummary -> {
//创建收货任务
WmsTasks wmsTasks = new WmsTasks();
//任务类型,拣货任务
wmsTasks.setTaskType(WarehouseDictEnum.TASK_TYPE_PICKING.getCode());
//任务状态,已创建
wmsTasks.setTaskStatus(WarehouseDictEnum.TASK_STATUS_CREATED.getCode());
//任务创建时间
wmsTasks.setCreateTime(new Date());
//任务号
wmsTasks.setTaskNumber(wmsTasksService.generateTaskCode());
//波次id
wmsTasks.setWaveOrderId(waveId);
//商品id
wmsTasks.setProductId(wmsWaveSkuSummary.getSkuId());
//商品待拣货数量
wmsTasks.setQuantity(wmsWaveSkuSummary.getAllocatedQuantity());
//来源储位编码
wmsTasks.setSourceLocationCode(wmsWaveSkuSummary.getLocationCode());
//来源仓库
wmsTasks.setSourceWarehouseId(wmsWaveMaster.getWarehouseId());
//目的仓库
wmsTasks.setTargetWarehouseId(wmsWaveMaster.getWarehouseId());
//商品批次号
wmsTasks.setBatchNumber(wmsWaveSkuSummary.getBatchNumber());
//目的储位编码
wmsTasks.setTargetLocationCode("");
//波次拣货明细id
wmsTasks.setWaveSkuSummaryId(wmsWaveSkuSummary.getId());
//完成数量为0
wmsTasks.setCompletedQuantity(0);
return wmsTasks;
}).collect(Collectors.toList());
//批量插入任务表
boolean b1 = wmsTasksService.saveBatch(wmsTaskList);
if (!b1) {
throw new JeecgBootException("批量插入任务表失败");
}
//循环查询,每次查询100条数据,直到记录数数为0
page = wmsOutOrdersAllocationService.selectAllocatedQuantityByWaveId(waveId, ++n, 100);
if (page.getRecords().isEmpty()) {
break;
}
}
//更新该波次下出库单状态为拣货中
LambdaUpdateWrapper<WmsOutOrders> updateWrapper = new LambdaUpdateWrapper<WmsOutOrders>()
.eq(WmsOutOrders::getWaveId, waveId)
.eq(WmsOutOrders::getStatus, WarehouseDictEnum.OUTBOUND_ALLOCATED.getCode())
.set(WmsOutOrders::getStatus, WarehouseDictEnum.OUTBOUND_PICKING.getCode());
boolean update2 = wmsOutOrdersService.update(null, updateWrapper);
if (!update2) {
throw new JeecgBootException("更新该波次下出库单状态为拣货中失败");
}
//根据波次id查询出库单id
List<WmsOutOrders> wmsOutOrders = wmsOutOrdersService.selectByWaveId(waveId);
List<String> orderIds = wmsOutOrders.stream().map(WmsOutOrders::getId).collect(Collectors.toList());
//更新该波次下出库单明细状态为拣货中
LambdaUpdateWrapper<WmsOutOrdersItems> updateWrapper2 = new LambdaUpdateWrapper<WmsOutOrdersItems>()
.in(WmsOutOrdersItems::getOrderId, orderIds)
.eq(WmsOutOrdersItems::getStatus, WarehouseDictEnum.OUTBOUND_DETAIL_ALLOCATED.getCode())
.set(WmsOutOrdersItems::getStatus, WarehouseDictEnum.OUTBOUND_PICKING.getCode());
boolean update1 = wmsOutOrdersItemsService.update(null, updateWrapper2);
if (!update1) {
throw new JeecgBootException("更新该波次下出库单明细状态为拣货中失败");
}
}
....5.4 测试
- 创建波次
- 创建拣货任务
- 进入拣货列表查看任务是否创建成功
6 拣货
6.1 拣货接口
1)执行流程
2)数据流
3)缺货登记
如果在拣货时发现缺货,不应该简单地放弃拣货或随意从其他货位拿货。必须遵循一个标准化的流程来确保库存数据的准确性。
进行如下操作:
- 检查货位标签:确认你所在的货位是否正确,是否看错了货位号。
- 仔细搜寻:检查货位深处、角落或相邻货位,货物可能被放错了位置。
- 检查容器:如果货位上是料箱或纸箱,打开纸箱检查内部是否还有剩余库存。
如果确实没有找到货物,此时进行缺货登记。


缺货登记会记录在缺货登记表

4)阅读代码并测试
参照流程图阅读代码并测试。
拣货接口如下:
package org.jeecg.modules.wms.wave.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.modules.wms.outorder.entity.WmsOutOrdersItems;
import org.jeecg.modules.wms.wave.service.IPickingTasksService;
import org.jeecg.modules.wms.wmstask.entity.WmsTasks;
import org.jeecg.modules.wms.wmstask.entity.WmsTasksRecords;
import org.jeecg.modules.wms.wmstask.service.IWmsTasksRecordsService;
import org.jeecg.modules.wms.wmstask.service.IWmsTasksService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@Tag(name="任务表")
@RestController
@RequestMapping("/wave/pickingTasks")
@Slf4j
public class PickingTasksController {
.....
@AutoLog(value = "拣货")
@Operation(summary="拣货")
@RequiresPermissions("wave:pick_task:addRecords")
@PostMapping(value = "/addRecords")
public Result<String> addRecords(@RequestBody WmsTasksRecords wmsTasksRecords) {
String taskId = wmsTasksRecords.getId();
wmsTasksRecords.setTaskId(taskId);
wmsTasksRecords.setId(null);
pickingTasksService.pick(wmsTasksRecords);
return Result.OK("添加成功!");
}
......6.2 完成拣货
1) controller
当拣货商品不缺货时,当波次下的拣货任务完成后波次状态自动改为“拣货完成”,波次拣货明细状态改为“拣货完成”。
如果商品缺货会进行缺货登记,拣货任务仅完成部分商品拣货,此时需要手动执行“完成拣货”。
具体的接口如下,阅读代码,梳理执行流程。
wms\wave\controller\PickingTasksController.java
package org.jeecg.modules.wms.wave.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.modules.wms.outorder.entity.WmsOutOrdersItems;
import org.jeecg.modules.wms.wave.service.IPickingTasksService;
import org.jeecg.modules.wms.wmstask.entity.WmsTasks;
import org.jeecg.modules.wms.wmstask.entity.WmsTasksRecords;
import org.jeecg.modules.wms.wmstask.service.IWmsTasksRecordsService;
import org.jeecg.modules.wms.wmstask.service.IWmsTasksService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@Tag(name="任务表")
@RestController
@RequestMapping("/wave/pickingTasks")
@Slf4j
public class PickingTasksController {
......
/**
* 完成拣货
* 当存在缺货时需要手动执行完成拣货操作
*/
@AutoLog(value = "波次主表-完成拣货")
@Operation(summary="波次主表-完成拣货")
@PostMapping(value = "/completePickTask")
public Result<String> completePickTask(@RequestParam(name="ids",required=true) String ids) {
List<String> waveIds = Arrays.asList(ids.split(","));
for (String waveId : waveIds) {
pickingTasksService.completePickTask(waveId);
}
return Result.OK("完成拣货成功!");
}
......2) service
ackage org.jeecg.modules.wms.wave.service.impl;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import jakarta.annotation.Resource;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.modules.wms.config.WarehouseDictEnum;
import org.jeecg.modules.wms.goods.entity.WmsProducts;
import org.jeecg.modules.wms.goods.service.IWmsProductsService;
import org.jeecg.modules.wms.inventory.service.IWmsInventoryService;
//import org.jeecg.modules.wms.inventory.service.impl.WmsInventoryTransByPick;
import org.jeecg.modules.wms.inventory.service.impl.WmsInventoryTransByPick;
import org.jeecg.modules.wms.inventory.vo.WmsInventoryTransParam;
import org.jeecg.modules.wms.outorder.entity.WmsOutOrders;
import org.jeecg.modules.wms.outorder.entity.WmsOutOrdersAllocation;
import org.jeecg.modules.wms.outorder.entity.WmsOutOrdersItems;
import org.jeecg.modules.wms.outorder.mapper.WmsOutOrdersAllocationMapper;
import org.jeecg.modules.wms.outorder.service.IWmsOutOrdersAllocationService;
import org.jeecg.modules.wms.outorder.service.IWmsOutOrdersItemsService;
import org.jeecg.modules.wms.outorder.service.IWmsOutOrdersService;
import org.jeecg.modules.wms.warehouse.entity.WmsStorageLocations;
import org.jeecg.modules.wms.warehouse.entity.WmsStorageZones;
import org.jeecg.modules.wms.warehouse.service.IWmsStorageLocationsService;
import org.jeecg.modules.wms.warehouse.service.IWmsStorageZonesService;
import org.jeecg.modules.wms.wave.entity.WmsShortageRegistration;
import org.jeecg.modules.wms.wave.entity.WmsWaveMaster;
import org.jeecg.modules.wms.wave.entity.WmsWaveSkuSummary;
import org.jeecg.modules.wms.wave.mapper.WmsWaveMasterMapper;
import org.jeecg.modules.wms.wave.service.IPickingTasksService;
import org.jeecg.modules.wms.wave.service.IWmsShortageRegistrationService;
import org.jeecg.modules.wms.wave.service.IWmsWaveMasterService;
import org.jeecg.modules.wms.wave.service.IWmsWaveSkuSummaryService;
import org.jeecg.modules.wms.wmstask.entity.WmsTasks;
import org.jeecg.modules.wms.wmstask.entity.WmsTasksRecords;
import org.jeecg.modules.wms.wmstask.service.IWmsTasksRecordsService;
import org.jeecg.modules.wms.wmstask.service.IWmsTasksService;
import org.jeecg.modules.wms.wmstask.service.impl.WmsTasksRecordsServiceImpl;
import org.jeecg.modules.wms.wmstask.service.impl.WmsTasksServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author Mr.M
* @version 1.0
* @description 拣货服务类
* @date 2025/6/6 8:31
*/
@Service
public class PickingTasksServiceImpl implements IPickingTasksService {
......
/**
* 完成拣货
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void completePickTask(String waveId){
//查询该波次下的拣货任务列表
LambdaQueryWrapper<WmsTasks> eq = new LambdaQueryWrapper<WmsTasks>()
.eq(WmsTasks::getWaveOrderId, waveId);
List<WmsTasks> wmsTasks = wmsTasksService.list(eq);
//找没有完成的拣货任务
List<WmsTasks> wmsTasks1 = wmsTasks.stream().filter(wmsTask -> !WarehouseDictEnum.TASK_STATUS_COMPLETED.getCode().equals(wmsTask.getTaskStatus())).collect(Collectors.toList());
//遍历未完成任务列表,判断是否进行缺货登记
for (WmsTasks wmsTask : wmsTasks1) {
if (wmsTask.getCompletedQuantity() < wmsTask.getQuantity()) {
//剩余拣货数量
int remainingQuantity = wmsTask.getQuantity() - wmsTask.getCompletedQuantity();
//查询缺货登记
WmsShortageRegistration shortageRegistration = wmsShortageRegistrationService.getById(wmsTask.getId());
if (shortageRegistration == null || shortageRegistration.getShortageQuantity()==null) {
throw new JeecgBootException("任务"+wmsTask.getTaskNumber()+"未进行缺货登记");
}
//如果缺货登记数量不等于剩余拣货数量
if (shortageRegistration.getShortageQuantity() != remainingQuantity) {
throw new JeecgBootException("任务"+wmsTask.getTaskNumber()+"的缺货登记数量与剩余拣货数量不一致");
}
//将任务状态更新为已拣货
wmsTask.setTaskStatus(WarehouseDictEnum.TASK_STATUS_COMPLETED.getCode());
boolean update = wmsTasksService.updateById(wmsTask);
if(! update){
throw new JeecgBootException("更新任务状态失败");
}
}
}
//只要存在一个未拣货完成则抛出异常
boolean b = wmsTasks.stream().anyMatch(wmsTask -> !WarehouseDictEnum.TASK_STATUS_COMPLETED.getCode().equals(wmsTask.getTaskStatus()));
if (b) {
throw new RuntimeException("波次下存在未完成拣货任务");
}
//更新波次下拣货明细表的状态为已拣货
wmsWaveSkuSummaryService.updatePickedStatus(waveId);
//更新波次主表的拣货状态,如果波次下拣货明细的状态为已拣货,则更新波次主表的拣货状态为拣货完成
wmsWaveMasterService.updatePickStatus(waveId);
}
.....6.3 分拣接口
分拣是仓库拣货流程中的一个关键环节,主要用于在波次拣货(如播种式拣货或波次拣货)后,将已汇总拣选的商品按照原始订单进行再分配和核对,确保每个订单的商品准确无误。
1)分拣执行流程
2)数据流
3)阅读代码并流程
参照流程图阅读代码并测试。
package org.jeecg.modules.wms.wave.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.modules.wms.outorder.entity.WmsOutOrdersItems;
import org.jeecg.modules.wms.wave.service.IPickingTasksService;
import org.jeecg.modules.wms.wmstask.entity.WmsTasks;
import org.jeecg.modules.wms.wmstask.entity.WmsTasksRecords;
import org.jeecg.modules.wms.wmstask.service.IWmsTasksRecordsService;
import org.jeecg.modules.wms.wmstask.service.IWmsTasksService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@Tag(name="任务表")
@RestController
@RequestMapping("/wave/pickingTasks")
@Slf4j
public class PickingTasksController {
......
/**
* 分拣
* @param wmsOutOrdersItemsList 出库单明细
*/
@AutoLog(value = "分拣")
@Operation(summary="分拣")
@PostMapping(value = "/completeSorting")
public Result<String> completeSorting(@RequestBody List<WmsOutOrdersItems> wmsOutOrdersItemsList) {
pickingTasksService.completeSorting(wmsOutOrdersItemsList);
return Result.OK("分拣完成!");
}
......分拣service:
package org.jeecg.modules.wms.wave.service.impl;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import jakarta.annotation.Resource;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.modules.wms.config.WarehouseDictEnum;
import org.jeecg.modules.wms.goods.entity.WmsProducts;
import org.jeecg.modules.wms.goods.service.IWmsProductsService;
import org.jeecg.modules.wms.inventory.service.IWmsInventoryService;
//import org.jeecg.modules.wms.inventory.service.impl.WmsInventoryTransByPick;
import org.jeecg.modules.wms.inventory.service.impl.WmsInventoryTransByPick;
import org.jeecg.modules.wms.inventory.vo.WmsInventoryTransParam;
import org.jeecg.modules.wms.outorder.entity.WmsOutOrders;
import org.jeecg.modules.wms.outorder.entity.WmsOutOrdersAllocation;
import org.jeecg.modules.wms.outorder.entity.WmsOutOrdersItems;
import org.jeecg.modules.wms.outorder.mapper.WmsOutOrdersAllocationMapper;
import org.jeecg.modules.wms.outorder.service.IWmsOutOrdersAllocationService;
import org.jeecg.modules.wms.outorder.service.IWmsOutOrdersItemsService;
import org.jeecg.modules.wms.outorder.service.IWmsOutOrdersService;
import org.jeecg.modules.wms.warehouse.entity.WmsStorageLocations;
import org.jeecg.modules.wms.warehouse.entity.WmsStorageZones;
import org.jeecg.modules.wms.warehouse.service.IWmsStorageLocationsService;
import org.jeecg.modules.wms.warehouse.service.IWmsStorageZonesService;
import org.jeecg.modules.wms.wave.entity.WmsShortageRegistration;
import org.jeecg.modules.wms.wave.entity.WmsWaveMaster;
import org.jeecg.modules.wms.wave.entity.WmsWaveSkuSummary;
import org.jeecg.modules.wms.wave.mapper.WmsWaveMasterMapper;
import org.jeecg.modules.wms.wave.service.IPickingTasksService;
import org.jeecg.modules.wms.wave.service.IWmsShortageRegistrationService;
import org.jeecg.modules.wms.wave.service.IWmsWaveMasterService;
import org.jeecg.modules.wms.wave.service.IWmsWaveSkuSummaryService;
import org.jeecg.modules.wms.wmstask.entity.WmsTasks;
import org.jeecg.modules.wms.wmstask.entity.WmsTasksRecords;
import org.jeecg.modules.wms.wmstask.service.IWmsTasksRecordsService;
import org.jeecg.modules.wms.wmstask.service.IWmsTasksService;
import org.jeecg.modules.wms.wmstask.service.impl.WmsTasksRecordsServiceImpl;
import org.jeecg.modules.wms.wmstask.service.impl.WmsTasksServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author Mr.M
* @version 1.0
* @description 拣货服务类
* @date 2025/6/6 8:31
*/
@Service
public class PickingTasksServiceImpl implements IPickingTasksService {
...
/**
* 完成分拣
* @param wmsOutOrdersItemsList 出库单明细
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void completeSorting(List<WmsOutOrdersItems> wmsOutOrdersItemsList) {
//转成数组
// String[] outOrderItemIdArray = outOrderItemIds.split(",");
//出库单idList
List<String> outOrderIds = new ArrayList<>();
//波次id List
List<String> waveIds = new ArrayList<>();
//遍历出库单明细id
for (WmsOutOrdersItems items : wmsOutOrdersItemsList) {
//根据出库单明细id查询出库单明细
WmsOutOrdersItems wmsOutOrdersItems = wmsOutOrdersItemsService.getById(items.getId());
if (wmsOutOrdersItems == null) {
throw new RuntimeException("未找到对应的出库单明细,id:"+items.getId());
}
//商品id
String skuId = wmsOutOrdersItems.getSkuId();
//拣货数量,页面传入
Integer pickedQuantity = items.getPickedQuantity();
//商品信息
WmsProducts wmsProducts = wmsProductsService.getById(skuId);
//如果状态为分拣完成状态,则不允许分拣
if (WarehouseDictEnum.OUTBOUND_DETAIL_PICKED.getCode().equals(wmsOutOrdersItems.getStatus())) {
throw new RuntimeException("出库单商品"+wmsProducts.getProductName()+"为拣货完成状态,不允许分拣");
}
//根据出库单id查询出库单
WmsOutOrders wmsOutOrders = wmsOutOrdersService.getById(wmsOutOrdersItems.getOrderId());
if (wmsOutOrders == null) {
throw new RuntimeException("未找到对应的出库单,id:"+wmsOutOrdersItems.getOrderId());
}
outOrderIds.add(wmsOutOrders.getId());
//波次id
String waveId = wmsOutOrders.getWaveId();
waveIds.add(waveId);
//向波次表增加分拣数量
wmsWaveMasterService.addSortingQuantity(waveId, pickedQuantity);
//更新出库单明细的拣货数量及状态更新为分拣完成
wmsOutOrdersItemsService.updatePickedQuantityAndStatus(items.getId(), pickedQuantity);
}
//更新出库单状态为分拣完成
wmsOutOrdersService.updatePickStatus(outOrderIds);
//更新该波次完成分拣订单的数量
wmsWaveMasterService.updateCompleteSortingOrderQuantity(waveIds);
}6.3 整体流程
测试拣货流程:
- 创建出库单
- 分配库存
- 创建波次
- 创建拣货任务
- 拣货
- 分拣
8 打包
8.1 按波次创建包裹
1) 执行流程
2) 表结构

3)数据流
4)阅读源码并测试
代码:wms\shipment\controller\WmsShipmentController.java
package org.jeecg.modules.wms.shipment.controller;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jeecgframework.poi.excel.ExcelImportUtil;
import org.jeecgframework.poi.excel.def.NormalExcelConstants;
import org.jeecgframework.poi.excel.entity.ExportParams;
import org.jeecgframework.poi.excel.entity.ImportParams;
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
import org.jeecg.common.system.vo.LoginUser;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.wms.shipment.entity.WmsShipmentDetail;
import org.jeecg.modules.wms.shipment.entity.WmsShipment;
import org.jeecg.modules.wms.shipment.vo.WmsShipmentPage;
import org.jeecg.modules.wms.shipment.service.IWmsShipmentService;
import org.jeecg.modules.wms.shipment.service.IWmsShipmentDetailService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.extern.slf4j.Slf4j;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.apache.shiro.authz.annotation.RequiresPermissions;
@Tag(name="包裹主表")
@RestController
@RequestMapping("/shipment/wmsShipment")
@Slf4j
public class WmsShipmentController {
.....
/**
* 根据波次创建包裹
*/
@AutoLog(value = "波次-创建包裹")
@Operation(summary="波次-创建包裹")
@PostMapping(value = "/createShipment")
public Result<String> createShipment(@RequestParam(name="id",required=true) String id) {
int shipment = wmsShipmentService.createShipment(id);
return Result.OK("创建包裹成功"+shipment+"个");
}
......8.2 完成打包
选择波次,完成该波次下的所有出库单的打包。
如果波次下所有出库单明细的拣货数量-打包数量=0则说明打包完成,更新:
出库单状态-->已打包
出库单的明细状态-->已打包
包裹状态-->已打包
波次状态-->已打包
如果存在一个出库单明细未打包则无法更新状态为已打包。
1)执行流程
2)阅读源码并测试
代码:wms\shipment\controller\WmsShipmentController.java
package org.jeecg.modules.wms.shipment.controller;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jeecgframework.poi.excel.ExcelImportUtil;
import org.jeecgframework.poi.excel.def.NormalExcelConstants;
import org.jeecgframework.poi.excel.entity.ExportParams;
import org.jeecgframework.poi.excel.entity.ImportParams;
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
import org.jeecg.common.system.vo.LoginUser;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.wms.shipment.entity.WmsShipmentDetail;
import org.jeecg.modules.wms.shipment.entity.WmsShipment;
import org.jeecg.modules.wms.shipment.vo.WmsShipmentPage;
import org.jeecg.modules.wms.shipment.service.IWmsShipmentService;
import org.jeecg.modules.wms.shipment.service.IWmsShipmentDetailService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.extern.slf4j.Slf4j;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.apache.shiro.authz.annotation.RequiresPermissions;
@Tag(name="包裹主表")
@RestController
@RequestMapping("/shipment/wmsShipment")
@Slf4j
public class WmsShipmentController {
......
/**
* 完成某波次的打包
*/
@AutoLog(value = "包裹主表-完成某波次的打包")
@Operation(summary="包裹主表-完成某波次的打包")
@PostMapping(value = "/completePackByWave")
public Result<String> completePackByWave(@RequestParam(name="ids",required=true) String ids) {
if(StringUtils.isEmpty(ids)){
throw new JeecgBootException("请选择波次");
}
String[] waveIdsArray = ids.split(",");
for (String waveId : waveIdsArray) {
wmsShipmentService.completePackByWave(waveId);
}
return Result.OK("完成打包成功");
}
......9 发货
9.1 分析
1) 异步生成运单
生成运单需要远程调用第三方快递平台的接口,并且第三方快递平台的接口会进行限流,如:30次/s调用,这里我们需要使用定时任务加多线程的方式异步完成运单生成,
具体的流程如下:
2)打印电子面单
生成运单号完成,WMS系统远程调用顺丰平台的云打印后台服务打印电子面单,调用前需要通过接口请求顺丰平台获取token。

3) 发货
9.2 对接第三方接口
1)第三方快递平台调研
国内业务首选
(1) 菜鸟物流
优势:
- 阿里生态整合(淘宝/天猫订单自动同步);
- 覆盖全国98%区域,支持电子面单、智能分单;
- 提供API标准化接口,WMS对接文档完善。
适用场景:电商仓配一体化,尤其适合阿里系商家。
注意:需企业支付宝认证,可能受平台规则限制。
(2) 快递鸟
优势:
- 聚合多家快递(顺丰、三通一达、京东等),一次对接多平台;
- 提供轨迹查询、运费预估、电子面单等全功能API;
- 按调用量计费,适合中小型企业。
适用场景:多快递比价需求,或希望减少重复对接的开发成本。
(3) 京东物流
优势:
- 仓配网络高效(尤其一二线城市次日达);
- 冷链、大件物流能力突出;
- 与京东POP店铺深度绑定。
适用场景:注重时效或主营京东渠道的商家。
跨境业务选择
(1) 国际快递(DHL/UPS/FedEx)
- 优势:全球覆盖,时效稳定,品牌信任度高。
- 劣势:成本高,适合高货值订单。
- 对接建议:通过官方API或第三方服务商(如ShipStation)中转。
(2) 燕文物流
- 优势:专注跨境电商,整合欧美专线、东南亚小包,价格优势明显。
- 适用场景:速卖通、eBay、Shopify等平台卖家。
决策建议
测试接口稳定性:申请各平台测试账号,验证电子面单打印、轨迹回传等核心功能。
成本对比:
- 菜鸟/快递鸟通常按单收费(0.01~0.05元/单);
- 直连顺丰API可能有最低消费限制。
技术评估:
- 确认WMS是否预置某平台插件(如用友、金蝶WMS常内置快递100);
- 自研系统建议选择RESTful API规范的平台(如顺丰、中通新版接口)。
最终方案:若国内电商为主,推荐快递鸟+菜鸟组合(覆盖多数快递且成本可控);跨境则优先燕文+国际快递API。
2) 测试顺丰接口
以上快递平台,基本都需要注册商户,需要具备营业执照,顺丰平台提供沙箱模式,方便我们学习。所以考虑教学需求,我们对接顺丰,生成快递单号、打印电子面单。
参考“顺丰接口对接”文档。
9.3 定时任务技术调研
1)技术调研
Spring内置定时任务
方案描述:
Spring框架提供了@Scheduled注解,是最简单直接的定时任务实现方式。
优点:
- 简单易用,无需额外依赖
- 与Spring Boot无缝集成
- 支持cron表达式、固定延迟、固定速率等多种配置方式
cron 表达式的写法参考“附件 Q&A”
缺点:
- 单机运行,不适合分布式环境
- 缺乏失败重试机制
- 任务管理功能有限
示例代码:
package org.jeecg.modules.wms.waybill.task;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ScheduledThreadPoolExecutor;
/**
* @author Mr.M
* @version 1.0
* @description 生成运单定时任务
* @date 2025/7/15 11:51
*/
@Component
public class WaybillTaskJob {
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void test1() {
}
@Scheduled(fixedRate = 3600000) // 每小时执行一次
public void test2() {
}
}Quartz框架
方案描述:
Quartz是一个功能强大的开源作业调度库,支持复杂的调度需求。
优点:
- 支持集群部署
- 强大的调度能力(支持cron表达式、日历排除等)
- 任务持久化(支持多种数据库)
- 故障转移和负载均衡
- 丰富的监听器和插件机制
缺点:
- 配置相对复杂
- 需要额外数据库支持集群功能
集成步骤:
- 添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>- 配置示例:
@Configuration
public class QuartzConfig {
@Bean
public JobDetail inventorySyncJobDetail() {
return JobBuilder.newJob(InventorySyncJob.class)
.withIdentity("inventorySyncJob")
.storeDurably()
.build();
}
@Bean
public Trigger inventorySyncJobTrigger() {
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0 0/30 * * * ?");
return TriggerBuilder.newTrigger()
.forJob(inventorySyncJobDetail())
.withIdentity("inventorySyncTrigger")
.withSchedule(scheduleBuilder)
.build();
}
}XXL-JOB
方案描述:
XXL-JOB是一个分布式任务调度平台,提供可视化管理和丰富的功能。
优点:
- 完善的分布式任务调度解决方案
- 提供管理界面,方便任务管理
- 支持动态调整、故障转移、失败重试
- 丰富的报警机制
- 任务日志和运行报表
缺点:
- 需要额外部署调度中心
- 相对于简单项目可能过于复杂
集成步骤:
- 部署XXL-JOB调度中心
- 添加依赖:
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>${最新版本}</version>
</dependency>- 配置示例:
@XxlJob("inboundOrderProcessJob")
public ReturnT<String> inboundOrderProcessJob(String param) {
// 处理入库订单逻辑
return ReturnT.SUCCESS;
}Elastic-Job
方案描述:
Elastic-Job是当当网开源的分布式调度解决方案,基于Zookeeper实现分布式协调。
优点:
- 分布式调度协调
- 弹性扩容缩容
- 失效转移
- 支持多种作业类型(Simple、Dataflow、Script)
缺点:
- 依赖Zookeeper
- 学习曲线较陡
技术选型
中小型系统:
- 使用使用Spring的
@Scheduled注解、Quartz框架 - 简单易用,Spring内置定时任务无需额外配置,Quartz还提供基本的分布式支持和持久化能力
- 使用使用Spring的
大型/企业级WMS系统:
- 使用XXL-JOB
- 提供完善的分布式调度、监控和管理功能
本项目使用Spring内置定时任务,后边微服务项目使用XXL-JOB,支持分布式系统。
注意事项
- 任务幂等性:无论选择哪种方案,确保定时任务的幂等性,最基本的需求是要避免任务重复执行。
- 异常处理:合理处理任务执行中的异常,避免影响后续执行
- 日志记录:详细记录任务执行日志,便于排查问题
- 资源占用:考虑任务执行对系统资源的影响,避免高峰时段执行资源密集型任务
2)测试定时任务
- 在你的 Spring Boot 主应用类或者任何一个配置类上添加
@EnableScheduling注解,以开启对定时任务的支持。

- 编写定时任务方法
package org.jeecg.modules.wms.waybill.task;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* @author Mr.M
* @version 1.0
* @description 定时任务类
* @date 2025/9/13 10:10
*/
@Component
@Slf4j
public class WaybillTaskJob {
@Scheduled(cron = "0/5 * * * * ?")
public void test1() {
System.out.println("执行定时任务...");
}
}- 启动工程,观察控制台是否成功执行定时任务。
9.4 多线程任务基础代码
9.4.1 初级版本
1)定义Callable
package org.jeecg.modules.wms.waybill.task.handler;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.modules.wms.shipment.service.IWmsShipmentService;
import org.jeecg.modules.wms.waybill.service.IWmsWaybillService;
import java.util.concurrent.Callable;
/**
* 测试任务执行体
*/
@Slf4j
public class SimpleThreadCallable implements Callable {
@Override
public Object call() throws Exception {
//当前线程名称
try {
String name = Thread.currentThread().getName();
log.info("{},生成运单",name);
Thread.sleep(10000);
return true;
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
}2)定义线程池
package org.jeecg.modules.wms.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 波次处理任务线程池
*
*/
@Configuration
public class ThreadPoolConfiguration {
@Bean("waybillThreadPool")
public ThreadPoolExecutor waybillThreadPool() {
int corePoolSize = 10; // 核心线程数
int maximumPoolSize = 30; // 最大线程数
long keepAliveTime = 30L; // 空闲线程存活时间
TimeUnit unit = TimeUnit.SECONDS; // 时间单位
int queueCapacity = 2000; // 队列容量
// 创建任务队列
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(queueCapacity);
// 创建拒绝策略
RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardPolicy();
// 创建线程池
return new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
Executors.defaultThreadFactory(),
handler
);
}
}线程池面试题参考:创新WMS面试题。
3) 定时任务
package org.jeecg.modules.wms.waybill.task;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ScheduledThreadPoolExecutor;
/**
* @author Mr.M
* @version 1.0
* @description 生成运单定时任务
* @date 2025/7/15 11:51
*/
@Component
public class WaybillTaskJob {
@Resource(name = "waybillThreadPool")
private ThreadPoolExecutor waybillThreadPool;
/**
* 测试
*
*/
@Scheduled(cron = "0/2 * * * * ?")
public void test1() {
log.info("==============开始执行定时任务=================");
for (int i=0;i<10;i++) {
//定义任务对象
SimpleThreadCallable simpleThreadCallable= new SimpleThreadCallable();
// 启动多个线程完成请求顺丰下单并获取运单
waybillThreadPool.submit(simpleThreadCallable);
}
}
}4)测试
启动工程,观察控制台是否成功通过线程池执行任务

从输出结果可以看出,定时任务正常启动并且成功启动线程执行任务。
问题:由于定时任务调度时间短,任务执行时间长,上一轮任务还没有完成时定时任务会继续调度,此时会在任务队列堆积大量的任务。
9.4.2 完善版本
1)问题分析
上边的版本有如下问题
- 定时任务每2秒执行一次,如果此时生成运单的线程还没有执行完成将启动新的线程去执行,继续下去任务队列也会积累很多的任务。
解决:如果上一次定时任务还没有执行完成,本次调度不再执行。
- 线程执行异常没有处理
解决:如果线程执行过程存在异常可取消本线程执行任务,并记录异常信息。
2)改造后代码
// 用于防重入
private volatile boolean isRunning = false;
@Scheduled(cron = "0/2 * * * * ?")
public void test2() {
// 防重入检查
if (isRunning) {
log.warn("上次任务仍在执行,跳过本次调度");
return;
}
log.info("==============开始执行定时任务=================");
long startTime = System.currentTimeMillis();
isRunning = true;
try {
// 检查线程池基本状态
if (waybillThreadPool.getQueue().size() > 1000) {
log.error("线程池队列堆积严重");
return;
}
//任务列表
List<Future<Boolean>> futures = new ArrayList<>(10);
// 提交任务
for (int i = 0; i < 10; i++) {
SimpleThreadCallable simpleThreadCallable = new SimpleThreadCallable();
Future submit = waybillThreadPool.submit(simpleThreadCallable);
futures.add(submit);
}
// 等待所有任务完成(最多30秒)
int successCount = 0;
for (Future<Boolean> future : futures) {
try {
Boolean result = future.get(30, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(result)) {
successCount++;
}
} catch (Exception e) {
log.error("等待任务结果异常", e);
future.cancel(true);
}
}
long totalTime = System.currentTimeMillis() - startTime;
log.info("任务执行完成,成功数量: 10, 总耗时: {}ms", successCount, totalTime);
} finally {
isRunning = false;
}
}下边测试test2(),测试前将test1()的@Scheduled注释,如下:

测试输出日志如下:

9.5 异步生成运单开发
9.5.1 请求顺丰下单
1)配置
拷贝课程资料下“源代码/后端/config”下的SfExpressConfig.java到org.jeecg.modules.wms.config包下
在application-dev.yml下配置顺丰参数
#顺丰接口参数
wms:
express_api:
client_code:
check_word:
call_url: https://sfapi-sbox.sf-express.com/std/service
monthly_card:示例:

2)生成运单service
package org.jeecg.modules.wms.waybill.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.modules.wms.outorder.entity.WmsOutOrders;
import org.jeecg.modules.wms.shipment.entity.WmsShipment;
import org.jeecg.modules.wms.shipment.entity.WmsShipmentDetail;
import java.util.Collection;
import java.util.List;
/**
* @Description: 运单服务接口类
* @Author: jeecg-boot
* @Date: 2025-06-13
* @Version: V1.0
*/
public interface IWmsWaybillService {
/**
* 请求顺丰生成指定订单的运单
* 首先请求顺丰下单,成功后生成运单
*/
public void generateWaybillForOrder(String orderId) throws Exception;
}实现方法:
package org.jeecg.modules.wms.waybill.service.impl;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.plugins.IgnoreStrategy;
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.extension.plugins.pagination.PageDTO;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.DateUtils;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.modules.wms.config.WarehouseDictEnum;
import org.jeecg.modules.wms.goods.entity.WmsProducts;
import org.jeecg.modules.wms.goods.service.IWmsProductsService;
import org.jeecg.modules.wms.inventory.service.impl.WmsInventoryTransByDeliver;
import org.jeecg.modules.wms.inventory.vo.WmsInventoryTransParam;
import org.jeecg.modules.wms.outorder.entity.WmsOutOrders;
import org.jeecg.modules.wms.outorder.entity.WmsOutOrdersItems;
import org.jeecg.modules.wms.outorder.service.IWmsOutOrdersItemsService;
import org.jeecg.modules.wms.outorder.service.IWmsOutOrdersService;
import org.jeecg.modules.wms.shipment.entity.WmsShipment;
import org.jeecg.modules.wms.shipment.entity.WmsShipmentDetail;
import org.jeecg.modules.wms.shipment.mapper.WmsShipmentDetailMapper;
import org.jeecg.modules.wms.shipment.mapper.WmsShipmentMapper;
import org.jeecg.modules.wms.shipment.service.IWmsShipmentDetailService;
import org.jeecg.modules.wms.shipment.service.IWmsShipmentService;
import org.jeecg.modules.wms.shipment.strategy.ShipmentGenerationStrategy;
import org.jeecg.modules.wms.shipment.strategy.ShipmentStrategyFactory;
import org.jeecg.modules.wms.shipment.vo.ShipmentGenerationResult;
import org.jeecg.modules.wms.wave.entity.WmsWaveMaster;
import org.jeecg.modules.wms.wave.service.IWmsWaveMasterService;
import org.jeecg.modules.wms.waybill.service.IWmsSfService;
import org.jeecg.modules.wms.waybill.service.IWmsWaybillService;
import org.jeecg.modules.wms.wmstask.entity.WmsTasksRecords;
import org.jeecg.modules.wms.wmstask.service.IWmsTasksRecordsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Service
@Slf4j
public class WmsWaybillServiceImpl implements IWmsWaybillService {
@Autowired
private IWmsShipmentService wmsShipmentService;
@Autowired
private IWmsOutOrdersItemsService wmsOutOrdersItemsService;
@Autowired
private IWmsTasksRecordsService wmsTasksRecordsService;
@Autowired
private WmsWaybillServiceImpl owner;
@Autowired
private IWmsSfService sfService;
/**
* 请求顺丰生成指定订单的运单
* 首先请求顺丰下单,成功后生成运单
*/
public void generateWaybillForOrder(String orderId) throws Exception {
//查询出库单
WmsOutOrders order = wmsOutOrdersService.getById(orderId);
//如果状态不是打包完成,则不能生成运单
if(!order.getStatus().equals(WarehouseDictEnum.OUTBOUND_PACKED.getCode())){
throw new JeecgBootException("出库单"+order.getOrderNo()+"状态不是打包完成,不能生成运单");
}
//如果运单已经生成,则不能重复生成
if(order.getCreatedWaybill().equals("1")){
throw new JeecgBootException("出库单"+order.getOrderNo()+"已生成运单,不能重复生成");
}
//查询该订单下的包裹
List<WmsShipment> shipments = wmsShipmentService.selectByOrderId(orderId);
int shipmentSize = shipments.size();
if(shipmentSize==0){
throw new JeecgBootException("出库单"+order.getOrderNo()+"没有包裹,不能生成运单");
}
//调用顺丰创建运单
List<String> waybillNos = sfService.requestSfCreateOrder(order, shipments);
//保存运单号
owner.saveWaybill(waybillNos, shipments);
}
/**
* 保存运单号
* @param waybillNos 运单号
* @param shipments 包裹
*/
@Transactional(rollbackFor = Exception.class)
public void saveWaybill(List<String> waybillNos,List<WmsShipment> shipments){
//如果运单号的数量和包裹数量不一致不能继续,抛出异常
if(waybillNos.size() != shipments.size()){
throw new RuntimeException("运单数量和包裹数量不一致");
}
//将运单号依次保存到包裹中
for (int i = 0; i < waybillNos.size(); i++) {
LambdaUpdateWrapper<WmsShipment> set = new LambdaUpdateWrapper<WmsShipment>()
.eq(WmsShipment::getId, shipments.get(i).getId())
.set(WmsShipment::getTrackingNo, waybillNos.get(i));
wmsShipmentService.update(null,set);
}
WmsShipment wmsShipment = shipments.get(0);
//订单id
String orderId = wmsShipment.getOrderId();
//更新订单"createdWaybill"为1
LambdaUpdateWrapper<WmsOutOrders> update = new LambdaUpdateWrapper<WmsOutOrders>()
.eq(WmsOutOrders::getId, orderId)
.set(WmsOutOrders::getCreatedWaybill, "1");
wmsOutOrdersService.update(null, update);
}
}3)请求顺丰下单
拷贝"课程资料/源代码/后端/config/SfExpressConfig.java"到org.jeecg.modules.wms.config包下。
service接口如下,请求顺丰创建运单号。
/**
* @author Mr.M
* @version 1.0
* @description 顺丰对象service
* @date 2025/7/16 8:11
*/
public interface IWmsSfService {
/**
* 请求快递平台获取运单号
* @param order
* @param shipments
* @return 运单号
* @throws Exception
*/
public List<String> requestSfCreateOrder(WmsOutOrders order, List<WmsShipment> shipments) throws Exception;
}实现类:
/**
* @author Mr.M
* @version 1.0
* @description 顺丰对象service
* @date 2025/7/16 8:26
*/
@Service
@Slf4j
public class IWmsSfServiceImpl implements IWmsSfService {
@Autowired
private IWmsOutOrdersItemsService wmsOutOrdersItemsService;
@Autowired
private IWmsProductsService wmsProductsService;
/**
* 请求快递平台获取运单号
*/
@Override
public List<String> requestSfCreateOrder(WmsOutOrders order,List<WmsShipment> shipments) throws Exception {
int shipmentSize = shipments.size();
//请求顺丰下单
//创建参数对象
SfOrderInfo sfOrderInfo = new SfOrderInfo();
/**
* {
* "cargoDetails": [{
* "amount": 308.0,
* "count": 1.0,
* "name": "君宝牌地毯",
* "unit": "个",
* "volume": 0.0,
* "weight": 0.1
* }],
* "contactInfoList": [{
* "address": "十堰市丹江口市公园路155号",
* "city": "十堰市",
* "company": "清雅轩保健品专营店",
* "contact": "张三丰",
* "contactType": 1,
* "county": "武当山风景区",
* "mobile": "17006805888",
* "province": "湖北省"
* }, {
* "address": "湖北省襄阳市襄城区环城东路122号",
* "city": "襄阳市",
* "contact": "郭襄阳",
* "county": "襄城区",
* "contactType": 2,
* "mobile": "18963828829",
* "province": "湖北省"
* }],
* "customsInfo": {},
* "expressTypeId": 2,
* "extraInfoList": [],
* "isOneselfPickup": 0,
* "language": "zh-CN",
* "orderId": "QIAO-20200618-00522",
* "parcelQty": 3,
* "payMethod": 1,
* "totalWeight": 6,
* "monthlyCard":7551234567
* }
*/
//包裹总重量
sfOrderInfo.setOrderId(order.getOrderNo());
sfOrderInfo.setOrderId(order.getOrderNo());
sfOrderInfo.setPayMethod(1);//寄付
sfOrderInfo.setParcelQty(shipments.size());//包裹数量
sfOrderInfo.setTotalWeight(shipments.stream().mapToDouble(WmsShipment::getTotalWeight).sum());//包裹总重
sfOrderInfo.setMonthlyCard(SfExpressUtil.getMonthly_card());
sfOrderInfo.setExpressTypeId(2);//顺丰标快
sfOrderInfo.setLanguage("zh-CN");
sfOrderInfo.setIsOneselfPickup(0);//客户不同意快件自取
//联系人信息
List<SfOrderInfo.ContactInfo> contactInfoList = new ArrayList<>();
//收件人信息
SfOrderInfo.ContactInfo contactInfo = new SfOrderInfo.ContactInfo();
contactInfo.setContactType(2);//1,寄件方信息 2,到件方信息
contactInfo.setContact(order.getConsignee());
contactInfo.setMobile(order.getContact());
contactInfo.setProvince(order.getShippingProvince());
contactInfo.setCity(order.getShippingCity());
contactInfo.setCounty(order.getShippingCounty());
contactInfo.setAddress(order.getShippingAddress());
contactInfoList.add(contactInfo);
//发件人信息即货主信息
//货主id
String ownerId = order.getOwnerId();
//查询货主信息
WmsCargoOwners cargoOwner = wmsCargoOwnersService.getById(ownerId);
SfOrderInfo.ContactInfo senderInfo = new SfOrderInfo.ContactInfo();
senderInfo.setContactType(1);//1,寄件方信息 2,到件方信息
senderInfo.setContact(cargoOwner.getOwnerName());
senderInfo.setMobile(cargoOwner.getPhone());
senderInfo.setProvince(cargoOwner.getCity());
senderInfo.setCity(cargoOwner.getCity());
senderInfo.setCounty(cargoOwner.getCountry());
senderInfo.setAddress(cargoOwner.getAddress());
contactInfoList.add(senderInfo);
//添加联系人信息
sfOrderInfo.setContactInfoList(contactInfoList);
//商品信息
List<SfOrderInfo.CargoDetail> cargoDetails = new ArrayList<>();
//查询出库单明细
List<WmsOutOrdersItems> wmsOutOrdersItems = wmsOutOrdersItemsService.selectByMainId(order.getId());
// 遍历
for (WmsOutOrdersItems item : wmsOutOrdersItems) {
//查询商品
WmsProducts product = wmsProductsService.getById(item.getSkuId());
SfOrderInfo.CargoDetail cargoDetail = new SfOrderInfo.CargoDetail();
cargoDetail.setName(product.getProductName());
cargoDetail.setWeight(product.getNetWeight());//单位重量
cargoDetail.setCount(item.getPackedQuantity());
cargoDetail.setVolume(0.0);//体积
cargoDetail.setAmount(308.0);//价格
cargoDetails.add(cargoDetail);
sfOrderInfo.setCargoDetails(cargoDetails);
}
//测试数据
// SfOrderInfo.CargoDetail cargoDetail = new SfOrderInfo.CargoDetail();
// cargoDetail.setName("君宝牌地毯");
// cargoDetail.setWeight(0.1);
// cargoDetail.setCount(1.0);
// cargoDetail.setVolume(0.0);
// cargoDetail.setAmount(308.0);
// cargoDetails.add(cargoDetail);
// sfOrderInfo.setCargoDetails(cargoDetails);
//生成json
String sfOrderInfoJson = JSON.toJSONString(sfOrderInfo);
String responseBody = SfExpressUtil.post(SfExpressUtil.EXP_RECE_CREATE_ORDER, sfOrderInfoJson);
/**
* {"apiErrorMsg":"","apiResponseID":"0001980D3635003FCDC966C657C6883F","apiResultCode":"A1000","apiResultData":"{\"success\":false,\"errorCode\":\"8016\",\"errorMsg\":\"重复下单\",\"msgData\":null}"}
*/
// HashMap hashMap = JSON.parseObject("{\"success\":false,\"errorCode\":\"8016\",\"errorMsg\":\"重复下单\",\"msgData\":null}", HashMap.class);
//将响应结果转换成SfApiResponse
SfApiResponse sfApiResponse = JSON.parseObject(responseBody, SfApiResponse.class);
String apiResultDataJson = sfApiResponse.getApiResultData();
SfApiResponse.ApiResultData1 apiResultData11 = JSON.parseObject(apiResultDataJson, SfApiResponse.ApiResultData1.class);
//如果重复下单则调用查询订单接口
if ("重复下单".equals(apiResultData11.getErrorMsg())) {
/**
* {
* "searchType": "1",
* "orderId": "QIAO-20200618-00522",
* "language": "zh-cn"
* }
*/
//查询订单请求对象
SfExpressUtil.SearchOrderParam searchOrderParam = new SfExpressUtil.SearchOrderParam();
searchOrderParam.setOrderId(order.getOrderNo());
searchOrderParam.setSearchType("1");
searchOrderParam.setLanguage("zh-cn");
String searchOrderResponseBody = SfExpressUtil.post(SfExpressUtil.EXP_RECE_SEARCH_ORDER_RESP, JSON.toJSONString(searchOrderParam));
/**
* {"apiErrorMsg":"","apiResponseID":"0001980D4990BD3FE464FACE9440A73F","apiResultCode":"A1000","apiResultData":"{\"success\":true,\"errorCode\":\"S0000\",\"errorMsg\":null,\"msgData\":{\"orderId\":\"OBD202506110007\",\"returnExtraInfoList\":null,\"waybillNoInfoList\":[{\"waybillType\":1,\"waybillNo\":\"SF7444497657631\"}],\"origincode\":\"710\",\"destcode\":\"719\",\"filterResult\":\"2\",\"remark\":null,\"routeLabelInfo\":[{\"code\":\"1000\",\"routeLabelData\":{\"waybillNo\":\"SF7444497657631\",\"sourceTransferCode\":\"710\",\"sourceCityCode\":\"710\",\"sourceDeptCode\":\"710\",\"sourceTeamCode\":\"\",\"destCityCode\":\"719\",\"destDeptCode\":\"719J\",\"destDeptCodeMapping\":\"\",\"destTeamCode\":\"009\",\"destTeamCodeMapping\":\"\",\"destTransferCode\":\"719\",\"destRouteLabel\":\"719-719J-011\",\"proName\":\"顺丰标快\",\"cargoTypeCode\":\"T6\",\"limitTypeCode\":\"T6\",\"expressTypeCode\":\"B1\",\"codingMapping\":\"K14\",\"codingMappingOut\":\"\",\"xbFlag\":\"0\",\"printFlag\":\"000000000\",\"twoDimensionCode\":\"MMM={'k1':'719','k2':'719J','k3':'009','k4':'T801','k5':'SF7444497657631','k6':'','k7':'de29da16'}\",\"proCode\":\"T801\",\"printIcon\":\"00000000\",\"abFlag\":\"\",\"destPortCode\":\"\",\"destCountry\":\"\",\"destPostCode\":\"\",\"goodsValueTotal\":\"\",\"currencySymbol\":\"\",\"cusBatch\":\"\",\"goodsNumber\":\"\",\"errMsg\":\"\",\"checkCode\":\"de29da16\",\"proIcon\":\"\",\"fileIcon\":\"\",\"fbaIcon\":\"\",\"icsmIcon\":\"\",\"destGisDeptCode\":\"719J\",\"newIcon\":null},\"message\":\"SF7444497657631:\"}],\"contactInfo\":null,\"clientCode\":\"Y2VL6F82\",\"serviceList\":null}}"}
*/
SfApiResponse sfApiResponse1 = JSON.parseObject(searchOrderResponseBody, SfApiResponse.class);
String apiResultDataJson2 = sfApiResponse1.getApiResultData();
SfApiResponse.ApiResultData1 apiResultData12 = JSON.parseObject(apiResultDataJson2, SfApiResponse.ApiResultData1.class);
String msgData = apiResultData12.getMsgData();
SfApiResponse.MsgData apiResultData1 = JSON.parseObject(msgData, SfApiResponse.MsgData.class);
List<SfApiResponse.WaybillNoInfo> waybillNoInfoList = apiResultData1.getWaybillNoInfoList();
if (waybillNoInfoList != null && waybillNoInfoList.size() == shipmentSize) {
//取出waybillNoInfoList中的运单号,得到List<String>
List<String> waybillNos = waybillNoInfoList.stream().map(SfApiResponse.WaybillNoInfo::getWaybillNo).collect(Collectors.toList());
return waybillNos;
}
}
return null;
}
}9.5.2 定时任务
1)定义Callable
package org.jeecg.modules.wms.waybill.task.handler;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.modules.wms.waybill.service.IWmsWaybillService;
import java.util.concurrent.Callable;
/**
* 波次任务执行体
*/
@Slf4j
public class WaybillThreadCallable implements Callable {
/**
* 出库单id
*/
private String orderId;
/**
* 包裹service
*/
private IWmsWaybillService wmsWaybillService;
public WaybillThreadCallable(IWmsWaybillService wmsWaybillService, String orderId) {
this.orderId = orderId;
this.wmsWaybillService = wmsWaybillService;
}
@Override
public Object call() throws Exception {
try {
//执行任务处理
wmsWaybillService.generateWaybillForOrder(orderId);
return true;
}catch (Exception e){
log.error("WaveThreadCallable:{},e:",orderId, e);
return false;
}
}
}2)定时任务类
package org.jeecg.modules.wms.waybill.task;
import com.baomidou.mybatisplus.core.metadata.IPage;
import jakarta.annotation.Resource;
import org.jeecg.modules.wms.config.WarehouseDictEnum;
import org.jeecg.modules.wms.outorder.entity.WmsOutOrders;
import org.jeecg.modules.wms.outorder.service.IWmsOutOrdersService;
import org.jeecg.modules.wms.waybill.service.IWmsWaybillService;
import org.jeecg.modules.wms.waybill.task.handler.WaybillThreadCallable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ScheduledThreadPoolExecutor;
/**
* @author Mr.M
* @version 1.0
* @description 生成运单定时任务
* @date 2025/7/15 11:51
*/
@Component
public class WaybillTaskJob {
/**
* 注入线程池
*/
@Resource(name = "waybillThreadPool")
private ThreadPoolExecutor waybillThreadPool;
// 用于防重入
private volatile boolean isRunning = false;
/**
* 注入出库单service
*/
@Autowired
private IWmsOutOrdersService wmsOutOrdersService;
@Autowired
private IWmsWaybillService wmsWaybillService;
/**
* 定时获取面单
* 首先获取打包完成的波次,调用顺丰接口进行下单,获取面单 pdf并存储至minio
*/
@Scheduled(cron = "0/20 * * * * ?")
public void createWaybills() {
// 防重入检查
if (isRunning) {
log.warn("上次任务仍在执行,跳过本次调度");
return;
}
log.info("==============开始执行定时任务=================");
long startTime = System.currentTimeMillis();
isRunning = true;
try {
// 检查线程池基本状态
if (waybillThreadPool.getQueue().size() > 1000) {
log.error("线程池队列堆积严重");
return;
}
//查询出库单状态为打包完成且未创建运单的出库单
WmsOutOrders wmsOutOrders = new WmsOutOrders();
wmsOutOrders.setStatus(WarehouseDictEnum.OUTBOUND_PACKED.getCode());
wmsOutOrders.setCreatedWaybill("0");
//todo:出库单表添加status、createdWaybill联合索引
IPage<WmsOutOrders> wmsOutOrdersIPage = wmsOutOrdersService.queryList(wmsOutOrders, 1, 1000);
List<WmsOutOrders> records = wmsOutOrdersIPage.getRecords();
if(records.size()<=0){
return;
}
//任务列表
List<Future<Boolean>> futures = new ArrayList<>(10);
// 提交任务
for (WmsOutOrders outOrders : records) {
//定义任务对象
WaybillThreadCallable waveThreadCallable = new WaybillThreadCallable(wmsWaybillService,outOrders.getId());
//定义FutureTask
FutureTask<Boolean> futureTask = new FutureTask<>(waveThreadCallable);
//添加到任务列表
futures.add(futureTask);
// 启动多个线程完成请求顺丰下单并获取运单
waybillThreadPool.submit(futureTask);
}
// 等待所有任务完成(最多30秒)
int successCount = 0;
for (Future<Boolean> future : futures) {
try {
Boolean result = future.get(30, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(result)) {
successCount++;
}
} catch (Exception e) {
log.error("等待任务结果异常", e);
future.cancel(true);
}
}
long totalTime = System.currentTimeMillis() - startTime;
log.info("任务执行完成,成功数量: 10, 总耗时: {}ms", successCount, totalTime);
} finally {
isRunning = false;
}
}
}9.5.3 测试
- 创建出库单并完成通过
- 对该出库单创建波次,并拣货完成
- 创建包裹完成
- 通过定时任务生成运单
通过页面查看出库单下的包裹是否成功生成运单

9.6 打印电子面单
1)查看接口


云打印流程图:

WMS系统远程调用顺丰平台的云打印后台服务,调用 时需要通过接口请求顺丰平台获取token。
顺丰平台提供SCPPrint.js供云打印使用(https://scp-tcdn.sf-express.com/prd/sdk/lodop/2.7/SCPPrint.js)
token注册
token注册,需要客户的后端系统使用接入码和校验码,调用丰桥网关接口获取token,token有效期为2小时。获取方法参考:OAuth2认证说明(https://open.sf-express.com/developSupport/976720?authId=0)
打印插件
需要客户在电脑安装打印插件,如过客户电脑插件已经安装过clodop打印插件,可不需要安装。
本项目使用如下报文格式:
请求报文3:子母单场景传值举例
{
"documents": [{
"masterWaybillNo":"SF1234567890999",
"seq":"1",
"sum":"3"},
{
"masterWaybillNo":"SF1234567890999",
"branchWaybillNo":"SF2234567890100",
"seq":"2",
"sum":"3"},
{
"masterWaybillNo":"SF1234567890999",
"branchWaybillNo":"SF2234567890101",
"seq":"3",
"sum":"3"}2)前端代码
前端代码已提供,如下:
views\wave\WmsWaveMasterPacked.vue
views\wave\components\WmsWaveSfPrintModal.vue
界面如下:

点击“打印电子面单”调用views\wave\WmsWaveMasterPacked.vue的如下方法,本方法请求后端获取token。
/**
* 打印面单
*/
function printWaybills() {
if (selectedRowKeys.value.length === 0) {
createMessage.warn('请选择波次')
return;
}
//只允许单选
if (selectedRowKeys.value.length > 1) {
createMessage.warn('一次只能选择一个波次')
return;
}
//请求后台获取token及运单号
printWaybill({waveId: selectedRowKeys.value[0]}, function (res) {
console.log(res.waybillNos)
console.log(res.token)
openSfModal(true, {
waybillNos: res.waybillNos,
token: res.token,
isUpdate: false,
showFooter: true,
});
})
}注意修改views\wave\components\WmsWaveSfPrintModal.vue的内容
下图中修改为自己的账号

下图的模板编码从自己的账号中获取

获取方式:


3)后端接口
完成如下接口开发:
package org.jeecg.modules.wms.waybill.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.modules.wms.shipment.service.IWmsShipmentService;
import org.jeecg.modules.wms.shipment.vo.PrintWaybillResult;
import org.jeecg.modules.wms.waybill.sf.SfExpressUtil;
import org.jeecg.modules.wms.waybill.service.IWmsWaybillService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.io.UnsupportedEncodingException;
import java.util.List;
@Tag(name="运单接口类")
@RestController
@RequestMapping("/shipment/waybill")
@Slf4j
public class WmsWaybillController {
.....
@AutoLog(value = "波次-打印电子面单")
@Operation(summary="波次-打印电子面单")
@GetMapping (value = "/printWaybill")
public Result<PrintWaybillResult> printWaybill(@RequestParam(name="waveId",required=true) String waveId) throws UnsupportedEncodingException {
//查询波次下所有出库单的运单号
List<String> strings = wmsWaybillService.selectWaybillNosByWaveId(waveId);
//请求顺丰拿token
String token = SfExpressUtil.getToken();
PrintWaybillResult printWaybillResult = new PrintWaybillResult();
printWaybillResult.setToken(token);
printWaybillResult.setWaybillNos(strings);
return Result.OK(printWaybillResult);
}开发完成并进行测试。
4)测试
进入波次管理-待发货页面,选择波次,打印电子面单

打印电子面单 时选择PDF虚拟打印机,下图使用的是WPS自带的PDF虚拟打印机。

如果打印列表中没有PDF虚拟打印机,可以启用内置的 Microsoft Print to PDF
Windows 10/11:
- 打开 控制面板 > 程序和功能 > 启用或关闭 Windows 功能。
- 勾选 Microsoft Print to PDF,点击 确定 并重启电脑。

测试效果图:

9.7 添加分布式锁
到目前我们已经完成通过定时任务和多线程远程调用顺丰接口创建运单,在生产环境中WMS系统会部署多实例组成一个集群,每个实例是一个JVM,每个实例都会启动定时任务并且通过多线程去创建运单,如何避免多个实例的线程重复创建运单呢?
解决这个问题我们需要用到分布式锁,学习分布式锁之前我们回顾一下Java同步锁。
9.7.1 Synchronized 同步锁
在多线程编程中,有时候为了避免多个线程去争抢相同的资源导致线程不安全,Java中使用synchronized 关键字实现同步锁保证线程安全。
下边是一个入门程序
public class SynchronizedDemo {
private int count = 0;
// 同步方法 - 保证同一时间只有一个线程可以访问此方法
public synchronized void increment() {
count++;
}
public static void main(String[] args) throws InterruptedException {
SynchronizedDemo demo = new SynchronizedDemo();
demo.doWork();
}
public void doWork() throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increment();
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increment();
}
}
});
// 启动两个线程
thread1.start();
thread2.start();
// 等待两个线程执行完成
thread1.join();
thread2.join();
// 打印最终结果
System.out.println("Count is: " + count);
}
}程序说明:
- 共享资源:
count变量是两个线程共享的资源。 - 同步方法:
increment()方法被声明为synchronized,这意味着同一时间只有一个线程可以执行这个方法。 - 线程操作:两个线程各对
count变量进行 10000 次递增操作。 - 预期结果:如果没有同步,最终结果可能会小于 20000。使用
synchronized后,结果总是正确的 20000。
进行如下测试:
如果将increment()方法前边的关键字synchronized 去掉最终结果可能会小于 20000。
其他同步方式:
除了同步方法,你还可以使用同步代码块:
public void increment() {
synchronized(this) { // 同步代码块
count++;
}
}或者使用不同的锁对象:
private Object lock = new Object();
public void increment() {
synchronized(lock) {
count++;
}
}这个简单的例子展示了 synchronized 的基本用法,可以帮助你理解如何保证多线程环境下的线程安全。
阅读程序并思考下边程序的运行结果:
下边的程序运行结果正确:
package org.jeecg.modules.wms.lock;
public class SynchronizedDemo2 {
private int count = 0;
private Object lock = new Object();
// 同步方法 - 保证同一时间只有一个线程可以访问此方法
public void increment() {
synchronized(lock){
count++;
}
}
public void increment2() {
synchronized(lock){
count++;
}
}
// public synchronized void increment() {
// count++;
// }
public static void main(String[] args) throws InterruptedException {
SynchronizedDemo2 demo = new SynchronizedDemo2();
demo.doWork();
}
public void doWork() throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increment();
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increment2();
}
}
});
// 启动两个线程
thread1.start();
thread2.start();
// 等待两个线程执行完成
thread1.join();
thread2.join();
// 打印最终结果
System.out.println("Count is: " + count);
}
}下边程序运行结果不正确:
package org.jeecg.modules.wms.lock;
public class SynchronizedDemo3 {
private int count = 0;
private Object lock = new Object();
private Object lock2 = new Object();
// 同步方法 - 保证同一时间只有一个线程可以访问此方法
public void increment() {
synchronized(lock){
count++;
}
}
public void increment2() {
synchronized(lock2){
count++;
}
}
// public synchronized void increment() {
// count++;
// }
public static void main(String[] args) throws InterruptedException {
SynchronizedDemo3 demo = new SynchronizedDemo3();
demo.doWork();
}
public void doWork() throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increment();
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increment2();
}
}
});
// 启动两个线程
thread1.start();
thread2.start();
// 等待两个线程执行完成
thread1.join();
thread2.join();
// 打印最终结果
System.out.println("Count is: " + count);
}
}9.7.2 分布式锁
1) 什么是分布式锁
synchronized只能保证同一个虚拟机中多个线程去争抢锁。

如果我们的定时任务程序部署在多个虚拟机,如何实现多个虚拟机中的线程去争抢资源,只能有一个线程去使用资源,此时需要用到分布式锁,如下图:

虚拟机都去抢占同一个锁,该锁已不属于某个虚拟机,而是单独部署,由多个虚拟机所共享,这种锁叫分布式锁。
对分布式的理解,可简单理解为分开部署,定时任务程序分开部署在不同的虚拟机,分布式锁也是单独部署在一个地方。
实现分布式锁的方案有很多,常用的如下:
1、基于数据库实现分布锁
利用数据库的唯一索引、行级锁的特点实现分布式锁。
多个线程同时向数据库插入主键相同的同一条记录,谁插入成功谁就获取锁;多个线程同时去更新相同的记录,利用行锁控制谁更新成功谁就抢到锁,方法如下:
在表中添加一个version版本字段,在更新时对比版本号,更新成功将版本号加1,SQL为:
update 表名 set 字段=值,version=version+1 where id =? and version =?2、基于redis实现锁
redis提供了分布式锁的实现方案,比如:SETNX、redisson等。
3、使用zookeeper实现
zookeeper是一个分布式协调服务,主要解决分布式程序之间的同步的问题。zookeeper的结构类似的文件目录,多线程向zookeeper创建一个子目录(节点)只会有一个创建成功,利用此特点可以实现分布式锁,谁创建该结点成功谁就获得锁。
2)SETNX实现分布式锁
我们项目选用Redis实现分布式锁,Redis中有一个命令叫SETNX,它的工作过程是去set一个不存在的key,多个线程去设置同一个key只会有一个线程设置成功,设置成功的的线程拿到锁。
SETNX命令语法: SET resource-name anystring NX EX max-lock-time 即可实现。
NX:表示key不存在才设置成功。
EX:设置过期时间
示例:
先启动一个客户端执行下边的命令:
表示设置lock001锁,value为001,过期时间为30秒
SET lock001 001 NX EX 30
再用另一个客户端执行相同的命令:

通过测试发现,使用SET NX命令同一个key只会有个线程执行成功。
下边是一个在java代码使用SETNX的例子:
在RedisTemplate中SET NX命令使用setIfAbsent方法实现。
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.TimeUnit;
public class SimplestLock {
private RedisTemplate<String, String> redisTemplate;
public SimplestLock(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
// 获取锁
public boolean lock(String key) {
return Boolean.TRUE.equals(
redisTemplate.opsForValue().setIfAbsent(
key,
"1",
30,
TimeUnit.SECONDS
)
);
}
// 释放锁
public void unlock(String key) {
redisTemplate.delete(key);
}
}
//使用锁
public void doSomething() {
SimplestLock lock = new SimplestLock(redisTemplate);
String lockKey = "resource:123";
try {
if (lock.lock(lockKey)) {
// 获取锁成功,执行业务逻辑
// ...
}
} finally {
lock.unlock(lockKey);
}
}阅读上边的代码分析下边的问题:
1、是否需要设置过期时间
当SETNX 设置key成功1,这个key(就是锁)需要设置过期时间吗?
如果不设置过期时间,当获取到了锁却没有成功执行释放锁,这个锁将会一直存在,其它线程无法获取这个锁。
所以执行set nx时要指定过期时间,具体调用的方法是:
redisTemplate.opsForValue().setIfAbsent(K var1, V var2, long var3, TimeUnit var5)
2、如何释放锁
释放锁分为两种情况:key到期自动释放,手动删除。
1)key到期自动释放的方法
因为锁设置了过期时间,key到期会自动释放,但是会存在一个问题就是 线程的操作还没有执行完时key到期了,此时其它线程就抢到锁了,最终执行了重复的业务操作。
怎么解决这个问题?
可以将key的到期时间设置的长一些,足以执行完成任务,这样做的问题是效率会低,这个时间值不好把控。
2)手动删除锁
如果是采用手动删除锁可能和key到期自动删除有所冲突,造成删除了别人的锁。
比如:当线程1还没有执行完时key过期了,此时其它线程占用了锁,当上一个线程执行查询数据库等业务操作完成后手动删除锁就把其它线程的锁给删除了。
虽然针对以上问题在SETNX中也有解决方案,但都非常麻烦,在生产环境中通过使用Redisson替代SETNX实现分布式锁。
3)Redisson
Redisson 是一个用于 Java 开发的 Redis 客户端和分布式锁框架,它不仅可以实现分布式锁,还可以实现分布式集合(如 List、Set、Map 等)和分布式对象(如 AtomicInteger、AtomicLong、CountDownLatch 等),简单理解就是将JVM中内存存储的List、Set、AtomicInteger这些对象使用Redis去存储和管理。
使用Redisson的基本用法如下:
// 创建Redisson客户端
RedissonClient redissonClient = Redisson.create();
// 获取名为myLock的分布式锁实例,通过此实例进行加锁、解锁
RLock lock = redissonClient.getLock("myLock");
try {
// 尝试获取锁,最多等待3秒,持锁时间为5秒
boolean isLockAcquired = lock.tryLock(3, 5, TimeUnit.SECONDS);
if (isLockAcquired ) {
try {
// 获取锁成功,执行业务逻辑
Thread.sleep(5000);
} finally {
// 释放锁
lock.unlock();
}
} else {
// 获取锁失败,处理相应逻辑
}
} catch (InterruptedException e) {
// 处理中断异常
}说明:
lock.tryLock方法是一种非阻塞获取锁的方式,没有获取锁可以直接返回,而lock.lock()是一种阻塞获取锁的方法,多个线程通过lock()方法获取锁,只有一个线程获取到锁,其它线程将阻塞等待。
通常lock.tryLock方法使用的更广泛。
- 使用tryLock方法获取锁时传3个参数:
- waitTime:尝试获取锁的最大等待时间,在这个时间范围内会不断地尝试获取锁,如果在
waitTime时间内未能获取到锁,则返回false。waitTime默认为-1,表示获取锁失败后立刻返回不重试。 - leaseTime:表示持锁的时间,即锁的自动释放时间。在获取锁成功后,锁会在
leaseTime时间后自动释放。如果在持锁的时间内未手动释放锁,锁也会在leaseTime时间后自动释放。 - TimeUnit:表示时间单位,可以是秒、毫秒等。
- tryLock方法返回值:
true:获取到了锁
false:未获取到锁
- 注意释放锁
获取到锁后的代码放在try中,在finally 中释放锁。
下边测试多个Java进程争抢同一个锁
使用Redisson需要加入依赖,在jeecg-boot-base-core下添加依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.7</version>
</dependency>在test下编写测试代码:
15个线程去争抢同一个锁“stockLock”,抢到锁的线程会占用锁10钞。
package org.jeecg.modules.wms.lock;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import java.util.concurrent.TimeUnit;
public class RedissonLockDemo {
// 模拟商品库存
private static int stock = 10;
public static void main(String[] args) {
// 1. 配置Redisson客户端
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setDatabase(0);
// 2. 创建Redisson客户端
RedissonClient redissonClient = Redisson.create(config);
// 模拟多个线程并发扣减库存
for (int i = 0; i < 15; i++) {
new Thread(() -> {
String threadName = Thread.currentThread().getName();
// 3. 获取分布式锁实例
RLock lock = redissonClient.getLock("stockLock");
try {
System.out.println(threadName + " 尝试获取锁...");
// 4. 尝试获取锁,最多等待1秒,锁持有时间为20秒
boolean isLockAcquired = lock.tryLock(1, 20, TimeUnit.SECONDS);
if (isLockAcquired) {
try {
System.out.println(threadName + " 获取锁成功,开始处理业务");
// 模拟业务处理
if (stock > 0) {
stock--;
System.out.println(threadName + " 扣减库存成功,剩余库存: " + stock);
} else {
System.out.println(threadName + " 库存不足,无法扣减");
}
// 模拟业务处理时间
Thread.sleep(10000);
} finally {
// 5. 释放锁
lock.unlock();
System.out.println(threadName + " 释放锁");
}
} else {
System.out.println(threadName + " 获取锁失败,放弃操作");
}
} catch (InterruptedException e) {
System.out.println(threadName + " 被中断");
Thread.currentThread().interrupt();
}
}, "Thread-" + i).start();
}
// 主线程等待所有子线程完成
try {
Thread.sleep(150000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 6. 关闭Redisson客户端
redissonClient.shutdown();
System.out.println("最终库存: " + stock);
}
}运行程序,观察日志,只有一个线程拿到了锁。
运行过程中使用redis客户端连接redis,查到stockLock锁如下:
它是一个Hash结构,key是生成的具体的锁名称,vlaue为1。

下边我们测试多个 java进程争抢同一个锁,在IDEA中复制运行配置:

分别运行这两个RedissonLockDemo和RedissonLockDemo(1)。
通过控制台可以发现,只有一个进程中的一个线程拿到锁成功扣减库存。
4) 看门狗机制
再回顾下使用SETNX的前两个问题如下:
- 锁过期被强占
当线程01执行业务逻辑的时间过长,锁到达过期时间则自动释放锁,此时线程02将获取锁成功,仍然存在并发问题。
- 锁被强删
当线程01执行业务逻辑的时间过长,锁到达过期时间则自动释放锁,此时线程02将获取锁成功,线程01执行完业务逻辑后删除了线程02的锁。
导致前两个问题的主要原因是任务执行时间过长,超过了锁的有效期,锁失效后被其它线程抢占,设置锁的过期时间为多少合适呢?
设置多少都不合适,这个问题Redisson提供了锁自动续期的功能,默认锁的过期时间为30秒,当任务没有执行完成时每隔10秒自动续期一次,这个机制就是Redisson的看门狗机制。
"看门狗机制"(Watchdog)是一种用于监测和维护锁的超时时间的机制,它可以确保在任务没有完成时对锁的过期时间进行自动续期,以避免任务没有完成时锁自动释放的问题。开启看门狗后针对当前锁创建一个线程执行延迟任务,默认每隔10秒将锁的过期时间重新续期为30秒。当任务结束,程序执行unlock()方法释放锁时会结束看门狗线程。
注意:任务结束一定要执行unlock()方法释放锁,否则看门狗线程一直进行续期,导致锁无法释放。
下边测试看门狗:
调用下边的方法都可以开启看门狗:
tryLock(long waitTime, -1,TimeUnit unit)传入leaseTime参数为-1可以开启看门狗。
下边进行测试:
修改tryLock的方法,leaseTime参数传入-1,开启看门狗。
boolean isLockAcquired = lock.tryLock(1, -1, TimeUnit.SECONDS);并且修改任务执行时长为40秒,加长执行时间,观察看门狗自动续期锁。

观察锁的有效期,发现在到达20秒时自动续期为30秒,当40秒任务执行结束释放锁。

9.7.3 使用分布式锁
1)配置Redisson
拷贝课程资料下“源代码/后端/core/RedissonConfiguration.java”到 jeecg-boot-base-core\src\main\java\org\jeecg\config下
2)修改代码
修改原有generateWaybillForOrder方法,添加分布式锁控制。
package org.jeecg.modules.wms.waybill.service.impl;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.plugins.IgnoreStrategy;
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.extension.plugins.pagination.PageDTO;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.DateUtils;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.modules.wms.config.WarehouseDictEnum;
import org.jeecg.modules.wms.goods.entity.WmsProducts;
import org.jeecg.modules.wms.goods.service.IWmsProductsService;
import org.jeecg.modules.wms.inventory.service.impl.WmsInventoryTransByDeliver;
import org.jeecg.modules.wms.inventory.vo.WmsInventoryTransParam;
import org.jeecg.modules.wms.outorder.entity.WmsOutOrders;
import org.jeecg.modules.wms.outorder.entity.WmsOutOrdersItems;
import org.jeecg.modules.wms.outorder.service.IWmsOutOrdersItemsService;
import org.jeecg.modules.wms.outorder.service.IWmsOutOrdersService;
import org.jeecg.modules.wms.shipment.entity.WmsShipment;
import org.jeecg.modules.wms.shipment.entity.WmsShipmentDetail;
import org.jeecg.modules.wms.shipment.mapper.WmsShipmentDetailMapper;
import org.jeecg.modules.wms.shipment.mapper.WmsShipmentMapper;
import org.jeecg.modules.wms.shipment.service.IWmsShipmentDetailService;
import org.jeecg.modules.wms.shipment.service.IWmsShipmentService;
import org.jeecg.modules.wms.shipment.strategy.ShipmentGenerationStrategy;
import org.jeecg.modules.wms.shipment.strategy.ShipmentStrategyFactory;
import org.jeecg.modules.wms.shipment.vo.ShipmentGenerationResult;
import org.jeecg.modules.wms.wave.entity.WmsWaveMaster;
import org.jeecg.modules.wms.wave.service.IWmsWaveMasterService;
import org.jeecg.modules.wms.waybill.service.IWmsSfService;
import org.jeecg.modules.wms.waybill.service.IWmsWaybillService;
import org.jeecg.modules.wms.wmstask.entity.WmsTasksRecords;
import org.jeecg.modules.wms.wmstask.service.IWmsTasksRecordsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Service
@Slf4j
public class WmsWaybillServiceImpl implements IWmsWaybillService {
.....
@Autowired
private RedissonClient redissonClient;
/**
* 请求顺丰生成指定订单的运单
* 首先请求顺丰下单,成功后生成运单
*/
public void generateWaybillForOrder(String orderId) throws Exception {
//使用redisson分布式锁控制本方法
//定义锁名称
String lockName = "ORDERS:CREATEWAYBILL:LOCK:"+orderId;
RLock lock = redissonClient.getLock(lockName);
// 尝试获取锁,最多等待3秒,使用看门狗机制
boolean isLockAcquired = lock.tryLock(3, -1, TimeUnit.SECONDS);
if(isLockAcquired){
try {
//查询出库单
WmsOutOrders order = wmsOutOrdersService.getById(orderId);
//如果状态不是打包完成,则不能生成运单
if(!order.getStatus().equals(WarehouseDictEnum.OUTBOUND_PACKED.getCode())){
throw new JeecgBootException("出库单"+order.getOrderNo()+"状态不是打包完成,不能生成运单");
}
//如果运单已经生成,则不能重复生成
if(order.getCreatedWaybill().equals("1")){
throw new JeecgBootException("出库单"+order.getOrderNo()+"已生成运单,不能重复生成");
}
//查询该订单下的包裹
List<WmsShipment> shipments = wmsShipmentService.selectByOrderId(orderId);
int shipmentSize = shipments.size();
if(shipmentSize==0){
throw new JeecgBootException("出库单"+order.getOrderNo()+"没有包裹,不能生成运单");
}
//调用顺丰创建运单
List<String> waybillNos = sfService.requestSfCreateOrder(order, shipments);
//保存运单号
owner.saveWaybill(waybillNos, shipments);
}finally {
lock.unlock();
}
}
}3)测试
修改generateWaybillForOrder(String orderId) 方法的代码进行测试
使用看门狗机制,模拟执行时间为50秒,并在获取锁成功和失败分别输出日志
public void generateWaybillForOrder(String orderId) throws Exception {
//定义锁名称
String lockName = "ORDERS:CREATEWAYBILL:LOCK:"+orderId;
RLock lock = redissonClient.getLock(lockName);
boolean b = lock.tryLock(3, -1, TimeUnit.SECONDS);
//当前线程
String threadName = Thread.currentThread().getName();
if(b){
log.info(threadName+"当前线程获取锁成功:{}",lockName);
try {
//查询出库单
WmsOutOrders order = wmsOutOrdersService.getById(orderId);
//如果状态不是打包完成,则不能生成运单
if(!order.getStatus().equals(WarehouseDictEnum.OUTBOUND_PACKED.getCode())){
throw new JeecgBootException("出库单"+order.getOrderNo()+"状态不是打包完成,不能生成运单");
}
//如果运单已经生成,则不能重复生成
if(order.getCreatedWaybill().equals("1")){
throw new JeecgBootException("出库单"+order.getOrderNo()+"已生成运单,不能重复生成");
}
//查询该订单下的包裹
List<WmsShipment> shipments = wmsShipmentService.selectByOrderId(orderId);
int shipmentSize = shipments.size();
if(shipmentSize==0){
throw new JeecgBootException("出库单"+order.getOrderNo()+"没有包裹,不能生成运单");
}
//调用顺丰创建运单
List<String> waybillNos = sfService.requestSfCreateOrder(order, shipments);
//保存运单号
owner.saveWaybill(waybillNos, shipments);
//模拟执行时间
Thread.sleep(50000);
}finally {
lock.unlock();
}
}else{
log.info("当前线程获取锁失败:{}",lockName);
}
}启动两个java进程,在新的启动脚本中添加启动参数,设置端口为18080
修改启动脚本,点击“Add VM Options”

输入:-Dserver.port=18080,如下图:

两个进程启动成功:

修改数据库出库单表中某个已经获取运单的出库单记录created_waybill字段值为“0”
启动工程,断点调试:
其中一个进程 获取锁成功

另一个进程获取锁失败

9.7.4 AOP编程优化锁定义
1)优化思路
在上边的代码中我们加入了分布式锁并且测试通过。
试想如果有多个这样的业务方法需要分布式锁控制,都需要重复加入定义锁、获取锁、释放锁这样的模板代码。
我们可以基于AOP去优化,将定义锁、获取锁、释放锁这些代码提取出来。
不同的业务定义锁的时间可能不同,也可能有些业务使用看门狗,所以我们定义一个注解去供用户设置参数去控制。
2)定义Lock注解
在jeecg-boot-base-core工程中定义Lock注解,如下:

package org.jeecg.common.aspect.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
/**
* @author Mr.M
* @version 1.0
* @description 分布式锁工具类
* @date 2023/7/23 22:48
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Lock {
/**
* 加锁key的表达式,支持表达式
*/
String formatter();
/**
* 加锁时长
*/
long time() default 5;
/**
* 阻塞超时时间,默认2分钟,当block为true的时候生效
*/
long waitTime() default 120;
/**
* 加锁时间单位
*/
TimeUnit unit() default TimeUnit.SECONDS;
/**
* 方法访问完后要不要解锁,默认自动解锁
*/
boolean unlock() default true;
/**
* 如果设定了true将在获取锁时阻塞等待waitTime时间
*/
boolean block() default false;
/**
* 是否启用自动续期,如果使用自动续期则unlock必须设置为true
*/
boolean startDog() default false;
}3)使用Lock注解
首先看一个Lock注解的使用方法,下边的黄色代码使用Lock注解实现分布式锁,实现看门狗机制。
/**
* 请求顺丰生成指定订单的运单
* 首先请求顺丰下单,成功后生成运单
*/
@Lock(formatter = "ORDERS:CREATEWAYBILL:LOCK:#{orderId}", startDog=true)
public void generateWaybillForOrder(String orderId) throws Exception {
//查询出库单
WmsOutOrders order = wmsOutOrdersService.getById(orderId);
//如果状态不是打包完成,则不能生成运单
if(!order.getStatus().equals(WarehouseDictEnum.OUTBOUND_PACKED.getCode())){
throw new JeecgBootException("出库单"+order.getOrderNo()+"状态不是打包完成,不能生成运单");
}
//如果运单已经生成,则不能重复生成
if(order.getCreatedWaybill().equals("1")){
throw new JeecgBootException("出库单"+order.getOrderNo()+"已生成运单,不能重复生成");
}
//查询该订单下的包裹
List<WmsShipment> shipments = wmsShipmentService.selectByOrderId(orderId);
int shipmentSize = shipments.size();
if(shipmentSize==0){
throw new JeecgBootException("出库单"+order.getOrderNo()+"没有包裹,不能生成运单");
}
//调用顺丰创建运单
List<String> waybillNos = sfService.requestSfCreateOrder(order, shipments);
//保存运单号
owner.saveWaybill(waybillNos, shipments);
//模拟时间
Thread.sleep(50000);
}3)定义分布式锁切面类
我们定义切面类实现分布式锁。
首先从“资料\源代码\后端\core”拷贝AspectUtils.java和SpelUtils.java到jeecg-boot-base-core\src\main\java\org\jeecg\common\util下。

然后定义切面类:

代码如下:
package org.jeecg.common.aspect;
import io.undertow.util.BadRequestException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.jeecg.common.aspect.annotation.Lock;
import org.jeecg.common.util.AspectUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import java.lang.reflect.Method;
/**
* @author Mr.M
* @version 1.0
* @description 分布式锁工具类
* @date 2023/7/23 22:56
*/
@Aspect
public class LockAspect {
private final RedissonClient redissonClient;
public LockAspect(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
@Around("@annotation(lock)")
public Object handleLock(ProceedingJoinPoint pjp, Lock lock) throws Throwable {
//锁key的格式化字符,此字符串中有spEL表达式
String formatter = lock.formatter();
Method method = AspectUtils.getMethod(pjp);
Object[] args = pjp.getArgs();
//得到锁的key
String redisKey = AspectUtils.parse(formatter, method, args);
//获取锁阻塞等待的时间,如果是0表示去尝试获取锁,如果获取不到则结束
long waitTime = 0;
//阻塞等待获取锁
if (lock.block()) {
//根据时间单位转换成ms
waitTime = lock.waitTime();
}
//加锁时长
long time = lock.time();
//启动看门狗自动续期
if(lock.startDog()){
time = -1;
//如果设置自动续期必须在方法执行后释放锁
if(!lock.unlock()){
throw new BadRequestException("请求参数不合法");
}
}
//得到锁对象
RLock rLock = redissonClient.getLock(redisKey);
//尝试加锁
boolean success = rLock.tryLock(waitTime,time, lock.unit());
if (!success && !lock.block()) {
//未阻塞要求的情况下未得到锁
throw new BadRequestException("操作频繁,请稍后重试");
}
if (!success) {
//阻塞情况下未得到锁,请求超时
throw new RuntimeException("请求超时");
}
try {
return pjp.proceed();
} finally {
if (lock.unlock()) {
rLock.unlock();
}
}
}
}4)声明切面类
从“资料\源代码\后端\core”拷贝LockConfiguration.java到jeecg-boot-base-core\src\main\java\org\jeecg\config下。

5) 测试
修改数据库出库单表中某个已经获取运单的出库单记录created_waybill字段值为“0”
启动多个java进程,观察日志,只有一个线程获取锁成功
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@34187891] was not registered for synchronization because synchronization is not active
2025-09-13 16:27:19.167 [pool-8-thread-2] INFO o.j.m.w.waybill.service.impl.WmsWaybillServiceImpl:203 - pool-8-thread-2当前线程获取锁成功
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7e8ce4ad] was not registered for synchronization because synchronization is not active
2025-09-13 16:27:34.170 [pool-8-thread-7] ERROR o.j.m.w.waybill.task.handler.WaybillThreadCallable:32 - 生成运单任务出错:操作频繁,请稍后重试9.8 发货
1)定义变更库存类
发货后需要对拣货储位的商品数量进行扣减,代码如下:
package org.jeecg.modules.wms.inventory.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.modules.wms.goods.entity.WmsProducts;
import org.jeecg.modules.wms.goods.service.IWmsProductsService;
import org.jeecg.modules.wms.inventory.entity.WmsInventory;
import org.jeecg.modules.wms.inventory.entity.WmsInventoryTrans;
import org.jeecg.modules.wms.inventory.mapper.WmsInventoryTransMapper;
import org.jeecg.modules.wms.inventory.service.IWmsInventoryService;
import org.jeecg.modules.wms.inventory.service.IWmsInventoryTransService;
import org.jeecg.modules.wms.inventory.vo.WmsInventoryTransParam;
import org.jeecg.modules.wms.warehouse.entity.WmsStorageLocations;
import org.jeecg.modules.wms.warehouse.service.IWmsStorageLocationsService;
import org.jeecg.modules.wms.warehouse.service.IWmsStorageZonesService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
/**
* @author Mr.M
* @version 1.0
* @description 发货执行库存变更
* @date 2025/8/12 16:11
*/
@Service
public class WmsInventoryTransByDeliver extends ServiceImpl<WmsInventoryTransMapper, WmsInventoryTrans> implements IWmsInventoryTransService {
@Autowired
private IWmsInventoryService wmsInventoryService;
//注入商品service
@Autowired
private IWmsProductsService wmsProductsService;
@Override
@Transactional(rollbackFor = Exception.class)
public void transfre(WmsInventoryTransParam inventoryTransParam) {
//非空判断,商品id、原始储位编码、目标储位编码、数量不能为空
if (StringUtils.isEmpty(inventoryTransParam.getProductId())
|| StringUtils.isEmpty(inventoryTransParam.getSourceLocationCode())
|| inventoryTransParam.getExecQuantity() == null) {
//抛出异常
throw new RuntimeException("请填写商品id、原始储位编码、目标储位编码、数量");
}
//查询商品信息
WmsProducts products = wmsProductsService.getById(inventoryTransParam.getProductId());
if (products == null) {
//抛出异常
throw new RuntimeException("商品id在商品表中不存在");
}
//===================源储位扣减库存=====================
/**
* ===拣货
* 拣货储位:
* 在库数量:减少
* 可用数量:0
* 分配数量:减少
*
*
*/
//根据源储位编码、商品id、容器编码查询库存表
WmsInventory wmsInventorySource = wmsInventoryService.getInventoryByUniqueKey(inventoryTransParam.getProductId(), inventoryTransParam.getSourceLocationCode(), inventoryTransParam.getBatchNumber());
//如果wmsInventorySource为空则抛出异常
if (wmsInventorySource == null) {
//抛出异常
throw new RuntimeException("源储位编码或商品id在库存中不存在");
}
LambdaUpdateWrapper<WmsInventory> wmsInventoryLambdaUpdateWrapper = new LambdaUpdateWrapper<WmsInventory>()
.eq(WmsInventory::getId, wmsInventorySource.getId())
.setSql("stock_quantity=stock_quantity-{0}", inventoryTransParam.getExecQuantity())
.setSql("allocated_quantity=allocated_quantity-{0}", inventoryTransParam.getExecQuantity())
.ge(WmsInventory::getStockQuantity,inventoryTransParam.getExecQuantity())
.ge(WmsInventory::getAllocatedQuantity,inventoryTransParam.getExecQuantity());
boolean update = wmsInventoryService.update(null, wmsInventoryLambdaUpdateWrapper);
if (!update) {
//抛出异常
throw new RuntimeException("更新库存失败");
}
//插入库存变更表
WmsInventoryTrans wmsInventoryTrans = new WmsInventoryTrans();
wmsInventoryTrans.setProductId(inventoryTransParam.getProductId());
wmsInventoryTrans.setLocationCode(inventoryTransParam.getSourceLocationCode());
wmsInventoryTrans.setBatchNumber(inventoryTransParam.getBatchNumber());
wmsInventoryTrans.setChangeQuantity(inventoryTransParam.getExecQuantity()*-1);
wmsInventoryTrans.setTransactionType(inventoryTransParam.getTransactionType());
wmsInventoryTrans.setRemarks(inventoryTransParam.getRemarks());
wmsInventoryTrans.setTransactionTime(new Date());
save(wmsInventoryTrans);
}
}2)阅读代码测试发货
发货接口如下:
@Tag(name="运单接口类")
@RestController
@RequestMapping("/shipment/waybill")
@Slf4j
public class WmsWaybillController {
....
/**
* 根据波次进行发货
*/
@AutoLog(value = "波次-发货")
@Operation(summary="波次-发货")
@GetMapping(value = "/send")
public Result<String> send(@RequestParam(name="waveId",required=true) String waveId) {
wmsWaybillService.send(waveId);
return Result.OK("发货成功!");
}
}3)修改代码
通过阅读代码,发货后将减少发货位的商品库存,发货位是通过拣货记录获取。
修改执行任务方法,在拣货任务记录中加上波次ID和波次拣货明细ID
package org.jeecg.modules.wms.wmstask.service.impl;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.PageDTO;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.DateUtils;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.modules.wms.config.WarehouseDictEnum;
import org.jeecg.modules.wms.inorder.entity.WmsStockInOrderItems;
import org.jeecg.modules.wms.inorder.entity.WmsStockInOrders;
import org.jeecg.modules.wms.inorder.service.IWmsStockInOrderItemsService;
import org.jeecg.modules.wms.inorder.service.IWmsStockInOrdersService;
import org.jeecg.modules.wms.inventory.service.impl.WmsInventoryTransByPutway;
import org.jeecg.modules.wms.inventory.service.impl.WmsInventoryTransByReceiving;
import org.jeecg.modules.wms.inventory.vo.WmsInventoryTransParam;
import org.jeecg.modules.wms.wmstask.entity.WmsTasks;
import org.jeecg.modules.wms.wmstask.entity.WmsTasksRecords;
import org.jeecg.modules.wms.wmstask.mapper.WmsTasksMapper;
import org.jeecg.modules.wms.wmstask.service.IWmsTasksRecordsService;
import org.jeecg.modules.wms.wmstask.service.IWmsTasksService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
@Service
public class WmsTasksServiceImpl extends ServiceImpl<WmsTasksMapper, WmsTasks> implements IWmsTasksService {
.....
@Override
public WmsTasks execute(WmsTasksRecords wmsTasksRecords) {
//任务id
String taskId = wmsTasksRecords.getTaskId();
//查询任务信息
WmsTasks wmsTasks = getById(taskId);
//计划数量
Integer quantity = wmsTasks.getQuantity();
//收货数量
Integer execQuantity = wmsTasksRecords.getExecQuantity();
//已完成数量
Integer completedQuantity = ObjectUtils.defaultIfNull(wmsTasks.getCompletedQuantity(), 0);
//如果已完成数量+执行数量大于计划数量则不能执行
if(completedQuantity + execQuantity > quantity){
throw new JeecgBootException("执行数量不能大于计划数量!");
}
/**
* 1.在任务表增加完成收货数量
* 2.添加任务执行记录
* 3.更新任务状态,如果完成数量等于计划数量则任务状态更新为完成
*/
//1.在任务表增加完成收货数量
//sql update wms_tasks set completed_quantity=completed_quantity+? where id=? and completed_quantity<=quantity-?
LambdaUpdateWrapper<WmsTasks> le = new LambdaUpdateWrapper<WmsTasks>().setSql("completed_quantity=completed_quantity+" + execQuantity)
.eq(WmsTasks::getId, wmsTasksRecords.getTaskId())
.le(WmsTasks::getCompletedQuantity, quantity - execQuantity);
boolean update = update(null, le);
if(!update){
throw new JeecgBootException("执行数量不能大于计划数量!");
}
//2.添加任务执行记录
//目标仓库id
wmsTasksRecords.setTargetWarehouseId(wmsTasks.getTargetWarehouseId());
//波次id
wmsTasksRecords.setWaveOrderId(wmsTasks.getWaveOrderId());
//波次明细id
wmsTasksRecords.setWaveSkuSummaryId(wmsTasks.getWaveSkuSummaryId());
//入库单id
wmsTasksRecords.setStockInOrderId(wmsTasks.getStockInOrderId());
//入库单明细id
wmsTasksRecords.setStockInOrderItemId(wmsTasks.getStockInOrderItemId());
//任务id
wmsTasksRecords.setTaskId(wmsTasks.getId());
//商品 id
wmsTasksRecords.setProductId(wmsTasks.getProductId());
//任务编码
wmsTasksRecords.setTaskNumber(wmsTasks.getTaskNumber());
//任务类型
wmsTasksRecords.setTaskType(wmsTasks.getTaskType());
//执行时间
wmsTasksRecords.setOperationTime(new Date());
//执行人
//获取当前用户
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
String userId = sysUser.getId();
wmsTasksRecords.setOperator(userId);
boolean save = wmsTasksRecordsService.save(wmsTasksRecords);
if(!save){
throw new JeecgBootException("添加任务执行记录失败!");
}
//更新任务状态,如果完成数量等于计划数量则任务状态更新为完成
wmsTasks = getById(taskId);
//完成数量
completedQuantity = wmsTasks.getCompletedQuantity();
//更新任务为已完成
if(completedQuantity==wmsTasks.getQuantity()) {
wmsTasks.setTaskStatus(WarehouseDictEnum.TASK_STATUS_COMPLETED.getCode());
boolean b = updateById(wmsTasks);
if(!b){
throw new JeecgBootException("更新任务状态失败!");
}
}
return wmsTasks;
}4)测试
- 创建出库单并审核通过
- 对该出库单创建波次,并拣货完成
- 创建包裹完成
- 通过定时任务生成运单
- 打印电子面单
- 发货
查询发货成功的波次:

进入详情页面
出库单状态为已发货

包裹状态为已发货

