
欢迎来到预见猿份,本站项目均为站长原创,学习中有问题可直接提交给站长老苗解决(微信:mrt_0607)。
苗润土老师,20余年一线项目经验,2014年加入黑马,星辰wms、云岚到家、学成在线项目作者,历任高级讲师、教学主管及课程研究员。 b站老苗
第二章 入库管理
目标
day01:
- 能够根据界面原型分析业务流程
- 能够说出入库的业务流程
- 能够说出入库模块的数据库表及关系
- 能够开发创建入库单功能
- 能够开发审核入库单功能
- 能够完善入库单列表
- 能够说出收货业务流程
- 能够开发创建收货任务接口
day02:
- 能够开发查询待收货接口
- 能够开发收货功能
- 能够开发查询收货记录功能
- 能够说出库存表的设计
- 能够开发存储库存接口
day03:
- 能够开发导入导出商品功能
- 能够开发导入导出库存
- 能够开发查询库存功能
- 能够说出web打印的技术方案
- 能够开发入库单打印功能
- 能够开发商品条码打印功能
day04:
能够自主分析上架模块并进行开发
1 需求分析
1.2 创建入库单
1)入库流程

- 创建入库单:ERP产生采购单(Purchase Order PO单)自动同步到WMS并创建入库单。
- 预约送货:供应商预约到货时间,供应商送货到仓库。
- 收货验收:核对PO单、检查货物数量、质量进行收货验收。
- 上架:使用PDA或PC完成货物上架,将货物存放到指定位置。
- 库存更新:货物信息(数量、批次、库位)录入系统,库存实时可查。
2)带问题阅读需求
入库操作首先需要创建入库单。
请阅读需求文档分析下边的问题:
创建入库单的流程是?
填写入库单基本信息:包括哪些信息?
填写入库单明细:包括哪些信息?
入库单基本信息与入库单明细的关系?
入库单状态有哪些?(知道多少说多少)
3)新增入库单

入库单类型:采购入库、退货入库、其它入库
入库单状态默认为:初始状态。
向入库单添加入库明细:

4)查询入库单

5)提交审核



入库单状态由初始状态、审核失败状态更新为“提交审核”
6)审核入库单


入库单状态由提交审核状态更新为“审核通过”、或“审核失败”
7) 删除入库单
入库单状态为初始状态、审核失败状态方可删除
流程:
- 进入入库单查询页面
- 选择一个入库单
- 点击“删除”
1.3 收货
1)带问题阅读需求
收货的业务流程是什么?
基于什么创建收货任务?一个入库单可以创建几个收货任务?
入库单与收货任务的关系?
入库单明细与收货任务的关系?
收货任务与收货记录的关系?
收货过程涉及到的入库单及入库单明细状态有哪些?
2)创建收货任务
根据入库单明细向任务表添加收货任务,有几条入库单明细添加几条收货任务。
创建收货任务完成,更新入库单状态为“收货中”, 更新入库单下的入库单明细状态为“收货中”.

选择执行人:

3)查询收货任务
查询任务类型为“收货任务”的任务,工人只允许查询指派给自己的任务。

详细信息:

4)执行收货
收货任务对应入库单明细,一个入库单明细可能包括多个批次的商品。
一个批次的商品收货一次,所以一个收货任务存在多次收货。
流程:
- 进入待收货查询页面
- 选择一个收货任务
- 点击“收货”
- 输入收货数量、储位等信息、批次号等
- 点击“确认”


收货完成更新入库单明细中的收货数量(良品数量)、不良品数量。
更新入库单明细状态:如果收货数量(良品数量)加不良品数量等于采购数量则更新入库单明细状态为“收货完成”,否则更新为“收货中”
更新入库单状态:如果该入库单所有明细的状态都为收货完成则更新入库单状态为“收货完成”,否则更为“收货中”
1.4 上架
1)带问题阅读需求
上架的业务流程?
什么时候能创建上架任务?
上架任务与谁对应?
上架需要填写哪些信息?
上架过程涉及到的入库单及入库单明细状态有哪些?
2)创建上架任务
当入库单收货任务完成后,根据该入库单的收货记录自动创建上架任务。
针对良品的收货记录去创建上架任务,有几条良品的收货记录就创建几条上架任务。
3)查询上架任务

4) 执行上架
流程:
- 进入上架任务菜单,查询待上架任务
- 选择一个任务,点击“上架”
- 输入上架储位及上架数量
- 点击“确认”


更新入库单明细中的上架数量及状态:
上架数量小于收货数量时:上架中
上架数量等于收货数量时:上架完成
更新入库单中的上架总量及状态
如果该入库单所有明细的状态都为上架完成则更新入库单状态为“上架完成”,否则更为“上架中”
2 模块设计
对入库管理模块进行整体设计,此工作通常由中、高级工程师进行,每位同学需要培养自己对模块整体设计的能力。
1)数据流
入库单---入库单明细:一对多
入库单---收货任务:一对多
入库单明细和收货任务:一对一
收货任务和收货记录:一对多
收货记录(良品)和上架任务:一对一
上架任务和上架记录:一对多

2)接口设计
根据需求文档设计接口:
入库单管理:
查询入库单
创建入库单
修改入库单
删除入库单
提交入库单
审核入库单
打印入库单
收货:
创建收货任务
查询待收货任务
收货
完成收货
查询收货记录
上架:
创建上架任务
查询待上架任务
上架
查询上架记录
3)准备环境
后端工程:
在创建分支前先提交现有代码到当前分支。
创建新分支: dev_02
切换到新分支:dev_02
从"课程资料/源码/后端"拷贝chuangxin-wms-java-course-dev02.zip,解压后,将其中的src目录覆盖dev_02分支下jeecg-module-wms工程下的src目录。

前端工程:
在创建分支前先提交现有代码到当前分支。
创建新分支: dev_02
切换到新分支:dev_02
从"课程资料/源码/前端"拷贝“前端dev_02.zip”,解压后将代码覆盖前端工程dev_02分支下src/view下的代码。
3 入库单管理
3.1 设计
1)表设计
入库单主表:存储入库单基本信息
入库单明细表:存储入库商品的信息
入库单与入库单明细:一对多关系

2)Online表单一对多
入库单与入库单明细为一对多关系,JeecgBoot提供一对多表的代码生成。
进入Online表单,先选择入库单明细表:

在外键中输入主表信息

再选择主表:
发现在附表一栏自动出现附表名称

通过功能测试查看效果
点击 入库单主表的 “更多-->功能测试”

新增界面:
上半部分是入库单信息,下半部分是入库单明细信息

3)生成后端代码
使用Online表单根据入库单表生成代码,对不符合的代码进行修改。
注意:包名为“inorder”

生成完成,将生成的代码拷贝到后端工程中

3.2 创建入库单
3.2.1 新增入库单
1)分析
根据需求文档,新增入库单仅向入库单主表添加记录,所以需要修改生成的后端代码。
新增入库单需要操作数据库:
向入库单表添加一条记录:默认初始状态
入库单号自动生成: 规则是 ASN+8位年月日+4位序号
2)service
新增添加入库单方法
/**
* @Description: 入库单主表
* @Author: jeecg-boot
* @Date: 2025-08-05
* @Version: V1.0
*/
public interface IWmsStockInOrdersService extends IService<WmsStockInOrders> {
/**
* 添加入库单
*/
public void add(WmsStockInOrders wmsStockInOrders);
...
}实现类:
@Service
public class WmsStockInOrdersServiceImpl extends ServiceImpl<WmsStockInOrdersMapper, WmsStockInOrders> implements IWmsStockInOrdersService {
@Autowired
private WmsStockInOrdersMapper wmsStockInOrdersMapper;
@Autowired
private WmsStockInOrderItemsMapper wmsStockInOrderItemsMapper;
@Autowired
private RedisUtil redisUtil;
/**
* 添加入库单
*/
public void add(WmsStockInOrders wmsStockInOrders){
//生成入库单号
String orderNumber = generateOrderNumber();
wmsStockInOrders.setOrderNumber(orderNumber);
//默认状态为初始状态
wmsStockInOrders.setStatus(WarehouseDictEnum.INBOUND_INITIAL.getCode());
wmsStockInOrdersMapper.insert(wmsStockInOrders);
}
public String generateOrderNumber() {
//当前8位时间戳(年月日), DateUtils.now()结果示例2001-11-11
String time = DateUtils.now().substring(0, 10).replace("-", "");
//key
String key = "wms:asn_number"+time;
long incr = redisUtil.incr(key, 1);
if(incr == 1){
//设置过期时间,设置24小时+10秒的目的是避免并发产生订单号重复
redisUtil.expire(key, 24*60*60+10);
}
//将incr组成4位字符串
String incrStr = String.format("%04d", incr);
String orderNumber = "ASN"+time+incrStr;
return orderNumber;
}
...
}3) controller
修改原有添加方法
@AutoLog(value = "入库单主表-添加")
@Operation(summary="入库单主表-添加")
@RequiresPermissions("inorder:wms_stock_in_orders:add")
@PostMapping(value = "/add")
public Result<String> add(@RequestBody WmsStockInOrdersPage wmsStockInOrdersPage) {
WmsStockInOrders wmsStockInOrders = new WmsStockInOrders();
BeanUtils.copyProperties(wmsStockInOrdersPage, wmsStockInOrders);
wmsStockInOrdersService.add(wmsStockInOrders);
return Result.OK("添加成功!");
}4) 测试
测试创建入库单功能,观察入库单号生成是否正确,表单填写的信息是否正常存储到数据库入库单表。
3.2.2 修改入库单
1)分析
修改入库单接口需要完成向入库单、入库单明细表写入数据。
入库单:修改数据
入库单明细:采用先删除再添加的方式
2)service
@Service
public class WmsStockInOrdersServiceImpl extends ServiceImpl<WmsStockInOrdersMapper, WmsStockInOrders> implements IWmsStockInOrdersService {
@Override
@Transactional(rollbackFor = Exception.class)
public void updateMain(WmsStockInOrders wmsStockInOrders,List<WmsStockInOrderItems> wmsStockInOrderItemsList) {
//初始状态、审核失败状态可以修改
if(!(WarehouseDictEnum.INBOUND_INITIAL.getCode().equals(wmsStockInOrders.getStatus())
|| WarehouseDictEnum.INBOUND_REJECTED.getCode().equals(wmsStockInOrders.getStatus()))){
throw new JeecgBootException("非初始状态、审核失败状态入库单不允许修改");
}
if(wmsStockInOrderItemsList==null || wmsStockInOrderItemsList.size()<=0){
//抛出异常
throw new JeecgBootException("请选择入库单明细");
}
//删除入库单明细数据
wmsStockInOrderItemsMapper.deleteByMainId(wmsStockInOrders.getId());
//对子表数据按商品分组,使用Stream流实现
Map<String, List<WmsStockInOrderItems>> collect = wmsStockInOrderItemsList.stream().collect(Collectors.groupingBy(WmsStockInOrderItems::getProductId));
//对List<WmsStockInOrderItems>进行合并
List<WmsStockInOrderItems> mergeList = new ArrayList<>();
collect.entrySet().stream().forEach(entry -> {
List<WmsStockInOrderItems> value = entry.getValue();
WmsStockInOrderItems merge = value.get(0);
//明细的状态为初始状态
merge.setStatus(WarehouseDictEnum.INBOUND_DETAIL_INITIAL.getCode());
//入库单主表id
merge.setOrderId(wmsStockInOrders.getId());
if(value!=null && value.size()>1){
merge.setExpectedQuantity(value.stream().mapToInt(WmsStockInOrderItems::getExpectedQuantity).sum());
mergeList.add( merge);
}else{
mergeList.add(merge);
}
});
int sum = mergeList.stream().mapToInt(WmsStockInOrderItems::getExpectedQuantity).sum();
wmsStockInOrders.setTotalExpectedQuantity(sum);
//添加子表数据
boolean b = wmsStockInOrderItemsService.saveBatch(mergeList);
if(!b){
throw new JeecgBootException("保存入库单明细失败");
}
//更新入库单
int i = wmsStockInOrdersMapper.updateById(wmsStockInOrders);
if(i<=0){
throw new JeecgBootException("更新入库单失败");
}
}
...
}3) 测试
测试流程:
创建入库单
修改入库单,添加采购商品,设置采购数据
预期结果:采购单信息及采购单明细保存成功。
3.2.3 修改入库单页面完善
1)需要修复的问题
进入修改入库单页面,商品名称为空。

首先找到前端页面显示商品名称位置

跟踪前端请求,结果中没有productName

2) 修改代码
找到接口

找到controller、service、mapper
修改mapper如下
首先在模型类添加productName
在WmsStockInOrderItems.java类添加如下:
@Schema(description="入库单明细")
@Data
@TableName("wms_stock_in_order_items")
public class WmsStockInOrderItems implements Serializable {
....
//商品名称
@TableField(exist = false)
private String productName;
//商品编码
@TableField(exist = false)
private String productCode;
...修改src\main\java\org\jeecg\modules\wms\inorder\mapper\xml\WmsStockInOrderItemsMapper.xml
<select id="selectByMainId" parameterType="java.lang.String" resultType="org.jeecg.modules.wms.inorder.entity.WmsStockInOrderItems">
SELECT a.*,wp.product_code,wp.product_name
FROM wms_stock_in_order_items a left join wms_products wp on a.product_id = wp.id
WHERE
a.order_id = #{mainId}
</select>3) 测试
预期结果,正常显示商品名称

3.3 实战
3.3.1 入库单列表
完善入库单查询列表,使空白字段正常显示,如下图:

3.3.2 审核入库单
参考需求文档、接口文档完成:提前审核入库单、审核入库单功能。
4 收货管理
4.1 模块设计
1)表设计
收货有收货任务,上架有上架任务,拣货有拣货任务,每次任务的执行需要记录任务的执行结果,这里将不同的任务抽取成统一的任务表与任务记录表。
任务表:记录了收货任务、上架任务、拣货任务的信息
任务记录表:记录了在收货、上架、拣货任务的执行记录。
任务与任务记录关系:一对多
举例:一个收货任务可能会分多次收货,同样一个上架任务也可以分多次上架
表结构如下图:

2)创建收货任务流程
创建收货任务的流程如下:
有什么约束?
收货任务与谁对应?
数据库操作有哪些?

3)收货流程
收货需要填写哪些信息?
什么情况收货完成?
执行收货的数据库操作有哪些?

4.2 创建收货任务
4.2.1 生成代码
1)生成后端代码
根据需求文档,创建收货任务需要向任务表写数据,所以需要生成任务表的代码。
在收货时需要向任务记录表写数据,所以也需要生成任务记录表的代码。
任务表和任务记录是收货、上架、拣货的共用表,包名使用wmstask。


生成完成,拷贝代码:

2)接口类重新规划
将生成的contorller类中的接口重新规划放在收货接口类ReceiveTasksController”中。
(后期开发上架接口再定义PutawayTasksController上架接口类)
复制下边的类到org.jeecg.modules.wms.inorder.controller中。
package org.jeecg.modules.wms.inorder.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
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 org.springframework.web.servlet.ModelAndView;
import java.util.Arrays;
/**
* @Description: 收货任务接口类
* @Author: jeecg-boot
* @Date: 2025-08-05
* @Version: V1.0
*/
@Tag(name="任务表")
@RestController
@RequestMapping("/inorder/receiveTasks")
@Slf4j
public class ReceiveTasksController {
@Autowired
private IWmsTasksService wmsTasksService;
@Autowired
private IWmsTasksRecordsService wmsTasksRecordsService;
/**
* 待收货任务查询
*
* @param wmsTasks
* @param pageNo
* @param pageSize
* @param req
* @return
*/
@Operation(summary="待收货任务查询")
@GetMapping(value = "/list")
public Result<IPage<WmsTasks>> queryPageList(WmsTasks wmsTasks,
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<WmsTasks> queryWrapper = QueryGenerator.initQueryWrapper(wmsTasks, req.getParameterMap());
Page<WmsTasks> page = new Page<WmsTasks>(pageNo, pageSize);
IPage<WmsTasks> pageList = wmsTasksService.page(page, queryWrapper);
return Result.OK(pageList);
}
@Operation(summary="收货记录查询")
@GetMapping(value = "/records")
public Result<IPage<WmsTasksRecords>> records(WmsTasksRecords wmsTasksRecords,
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<WmsTasksRecords> queryWrapper = QueryGenerator.initQueryWrapper(wmsTasksRecords, req.getParameterMap());
Page<WmsTasksRecords> page = new Page<WmsTasksRecords>(pageNo, pageSize);
IPage<WmsTasksRecords> pageList = wmsTasksRecordsService.page(page, queryWrapper);
return Result.OK(pageList);
}
/**
* 创建收货任务
* 参数: 入库单id,执行人
*/
@AutoLog(value = "创建收货任务")
@Operation(summary="创建收货任务")
@RequiresPermissions("inorder:receive_task:add")
@PostMapping(value = "/add")
public Result<String> add(String orderIds, String operator){
return Result.OK("创建成功!");
}
/**
* 收货
*
* @param wmsTasksRecords
* @return
*/
@AutoLog(value = "收货")
@Operation(summary="收货")
@RequiresPermissions("inorder:receive_task:addRecords")
@PostMapping(value = "/addRecords")
public Result<String> addRecords(@RequestBody WmsTasksRecords wmsTasksRecords) {
return Result.OK("添加成功!");
}
/**
* 通过id查询
*
* @param id
* @return
*/
//@AutoLog(value = "任务表-通过id查询")
@Operation(summary="任务表-通过id查询")
@GetMapping(value = "/queryById")
public Result<WmsTasks> queryById(@RequestParam(name="id",required=true) String id) {
WmsTasks wmsTasks = wmsTasksService.getById(id);
if(wmsTasks==null) {
return Result.error("未找到对应数据");
}
return Result.OK(wmsTasks);
}
}删除wmstask包下controller下的接口类

4.2.2 创建收货任务
1)controller
package org.jeecg.modules.wms.inorder.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
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 org.springframework.web.servlet.ModelAndView;
import java.util.Arrays;
@Tag(name="任务表")
@RestController
@RequestMapping("/inorder/receiveTasks")
@Slf4j
public class ReceiveTasksController {
.....
/**
* 创建收货任务
* 参数: 入库单id,执行人
*/
@AutoLog(value = "创建收货任务")
@Operation(summary="创建收货任务")
@RequiresPermissions("inorder:receive_task:add")
@PostMapping(value = "/add")
public Result<String> add(String orderIds, String operator){
if(StringUtils.isEmpty(orderIds) || StringUtils.isEmpty( operator)){
return Result.error("请选择入库单及执行人!");
}
String[] orderIdArray = orderIds.split(",");
for (String orderId : orderIdArray){
wmsTasksService.createReceiveTask(orderId, operator);
}
return Result.OK("创建成功!");
}
.....
}2)service
/**
* @Description: 任务表
* @Author: jeecg-boot
* @Date: 2025-08-05
* @Version: V1.0
*/
public interface IWmsTasksService extends IService<WmsTasks> {
/**
* 创建收货任务
* @param inOrderId 入库单id
* @param operator 执行人
*/
public void createReceiveTask(String inOrderId,String operator);
}实现方法:
package org.jeecg.modules.wms.wmstask.service.impl;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
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.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.wmstask.entity.WmsTasks;
import org.jeecg.modules.wms.wmstask.mapper.WmsTasksMapper;
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;
/**
* @Description: 任务表
* @Author: jeecg-boot
* @Date: 2025-08-05
* @Version: V1.0
*/
@Service
public class WmsTasksServiceImpl extends ServiceImpl<WmsTasksMapper, WmsTasks> implements IWmsTasksService {
@Autowired
private IWmsStockInOrdersService stockInOrdersService;
@Autowired
private IWmsStockInOrderItemsService stockInOrderItemsService;
@Autowired
private RedisUtil redisUtil;
@Override
@Transactional(rollbackFor = Exception.class)
@Transactional(rollbackFor = Exception.class)
public void createReceiveTask(String inOrderId, String operator) {
//查询入库单信息
WmsStockInOrders stockInOrders = stockInOrdersService.getById(inOrderId);
//仓库
String warehouseId = stockInOrders.getWarehouseId();
//入库单状态
String status = stockInOrders.getStatus();
//入库单状态非审核通过时不允许创建收货任务
if(!WarehouseDictEnum.INBOUND_APPROVED.getCode().equals(status)){
throw new JeecgBootException("入库单审核通过方可创建收货任务");
}
//根据入库单id查询入库单明细列表
List<WmsStockInOrderItems> stockInOrderItems = stockInOrderItemsService.selectByMainId(inOrderId);
//根据入库明细列表创建收货任务
for (WmsStockInOrderItems stockInOrderItem : stockInOrderItems) {
//创建收货任务
WmsTasks wmsTasks = new WmsTasks();
//任务类型,收货任务
wmsTasks.setTaskType(WarehouseDictEnum.TASK_TYPE_RECEIVING.getCode());
//任务状态,已创建
wmsTasks.setTaskStatus(WarehouseDictEnum.TASK_STATUS_CREATED.getCode());
//任务创建时间
wmsTasks.setCreateTime(new Date());
//任务号
wmsTasks.setTaskNumber(generateTaskCode());
//目的仓库
wmsTasks.setTargetWarehouseId(warehouseId);
//入库单id
wmsTasks.setStockInOrderId(stockInOrderItem.getOrderId());
//入库单明细id
wmsTasks.setStockInOrderItemId(stockInOrderItem.getId());
//商品id
wmsTasks.setProductId(stockInOrderItem.getProductId());
//商品采购数量
wmsTasks.setQuantity(stockInOrderItem.getExpectedQuantity());
//完成数量为0
wmsTasks.setCompletedQuantity(0);
//执行人
wmsTasks.setOperator(operator);
//创建任务
save(wmsTasks);
}
//更新入库单状态为收货中
WmsStockInOrders stockInOrdersUpdate = new WmsStockInOrders();
stockInOrdersUpdate.setId(inOrderId);//入库单id
stockInOrdersUpdate.setStatus(WarehouseDictEnum.INBOUND_RECEIVING.getCode());//收货中
boolean b = wmsStockInOrdersService.updateById(stockInOrdersUpdate);
if(!b){
throw new JeecgBootException("创建收货任务过程中更新入库单状态失败");
}
//根据入库单id更新入库单明细状态为收货中
boolean update = wmsStockInOrderItemsService.update(null,
new LambdaUpdateWrapper<WmsStockInOrderItems>().eq(WmsStockInOrderItems::getOrderId, inOrderId)
.set(WmsStockInOrderItems::getStatus, WarehouseDictEnum.INBOUND_DETAIL_RECEIVING.getCode()));
if(!update){
throw new JeecgBootException("创建收货任务过程中更新入库单明细状态失败");
}
}
/**
* 生成任务编号
* 规则: TSK+年月日+5位序号,序号使用redis自增序号实现
*/
public String generateTaskCode() {
//参考上边的代码实现
String time = DateUtils.now().substring(0, 10).replace("-", "");
String key = "tsk_number"+time;
long incr = redisUtil.incr(key, 1);
if(incr == 1){
//设置过期时间,设置24小时+10秒的目的是避免并发产生订单号重复
redisUtil.expire(key, 24*60*60+10);
}
//将incr组成4位字符串
String incrStr = String.format("%05d", incr);
String taskNumber = "TSK"+time+incrStr;
return taskNumber;
}
}3) 测试
测试流程:
创建入库单
完成审核
创建收货任务
查看任务表是否正确写入
4.2 收货
4.2.1 查询待收货任务(mapper)
先查询待收货记录,再进行收货,页面如下:
我们需要解决页面显示不完整的问题。

1)SQL
根据需求文档,查询待收货任务需要关联查询仓库名称、入库单信息、货主信息、商品信息等,这里写sql关联查询想要的结果。
select a.*, ww.warehouse_name, b.product_name, wco.owner_name, wsio.order_number stock_in_order_number
from wms_tasks a
left join wms_warehouses ww on a.target_warehouse_id = ww.id
left join wms_products b on a.product_id = b.id
left join wms_cargo_owners wco on b.owner_id = wco.id
left join wms_stock_in_orders wsio on a.stock_in_order_id = wsio.id
where a.operator = ?
order by a.create_time desc2) 完善模型类
根据sql查询的字段,需要完善映射类型的属性
@Data
@TableName("wms_tasks")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@Schema(description="任务表")
public class WmsTasks implements Serializable {
.....
//货主名称
@TableField(exist = false)
private java.lang.String ownerName;
//商品名称
@TableField(exist = false)
private java.lang.String productName;
//入库单号
@TableField(exist = false)
private java.lang.String stockInOrderNumber;
//仓库名称
@TableField(exist = false)
private java.lang.String warehouseName;
}3)定义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.wmstask.mapper.WmsTasksMapper">
<!--关联查询wms_task-->
<select id="queryTaskList" parameterType="org.jeecg.modules.wms.wmstask.entity.WmsTasks" resultType="org.jeecg.modules.wms.wmstask.entity.WmsTasks">
select a.*,ww.warehouse_name ,b.product_name,wco.owner_name,wsio.order_number stock_in_order_number
from wms_tasks a
left join wms_warehouses ww on a.target_warehouse_id=ww.id
left join wms_products b on a.product_id = b.id
left join wms_cargo_owners wco on b.owner_id = wco.id
left join wms_stock_in_orders wsio on a.stock_in_order_id = wsio.id
left join wms_stock_in_order_items wsioi on a.stock_in_order_item_id = wsioi.id
where 1=1
<!--精确查询任务号-->
<if test="taskNumber != null and taskNumber !=''">
and a.task_number = #{taskNumber}
</if>
<if test="taskType != null and taskType !=''">
and a.task_type = #{taskType}
</if>
<if test="taskStatus != null and taskStatus !=''">
and a.task_status = #{taskStatus}
</if>
<if test="stockInOrderNumber != null and stockInOrderNumber !=''">
and wsio.order_number = #{stockInOrderNumber}
</if>
<!--根据波次id查询-->
<if test="waveOrderId != null and waveOrderId !=''">
and a.wave_order_id = #{waveOrderId}
</if>
<!--执行人-->
<if test="operator != null and operator !=''">
and a.operator = #{operator}
</if>
order by a.create_time desc
</select>
</mapper>mapper接口
/**
* @Description: 任务表
* @Author: jeecg-boot
* @Date: 2025-08-05
* @Version: V1.0
*/
public interface WmsTasksMapper extends BaseMapper<WmsTasks> {
//关联查询任务列表
List<WmsTasks> queryTaskList(WmsTasks wmsTasks);
}4.2.2 查询待收货任务
1) controller
@Operation(summary="待收货任务查询")
@GetMapping(value = "/list")
public Result<IPage<WmsTasks>> receivelist(WmsTasks wmsTasks,
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
HttpServletRequest req) {
//当前用户id
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
wmsTasks.setOperator(sysUser.getId());
IPage<WmsTasks> wmsTasksIPage = wmsTasksService.list(wmsTasks, pageNo, pageSize);
return Result.OK(wmsTasksIPage);
}2) service
public interface IWmsTasksService extends IService<WmsTasks> {
/**
* 查询待办理任务列表
* @param wmsTasks
* @return
*/
public IPage<WmsTasks> list(WmsTasks wmsTasks, Integer pageNo, Integer pageSize);
...
}实现方法:
/**
* 分页查询任务列表
* @param wmsTasks
* @return
*/
@Override
public IPage<WmsTasks> list(WmsTasks wmsTasks, Integer pageNo, Integer pageSize) {
Page<WmsTasks> page = PageHelper.startPage(pageNo, pageSize);
List<WmsTasks> list = baseMapper.queryTaskList(wmsTasks);
PageDTO<WmsTasks> wmsTasksPageDTO = new PageDTO<>();
wmsTasksPageDTO.setRecords(list);
wmsTasksPageDTO.setTotal(page.getTotal());
wmsTasksPageDTO.setSize(page.getPageSize());
wmsTasksPageDTO.setCurrent(page.getPageNum());
wmsTasksPageDTO.setPages(page.getPages());
return wmsTasksPageDTO;
}3)测试
查询待收货任务如下:

任务类型数据字典:task_type
任务状态数据字典:task_status
请同学们课下解决上图中数据字典不显示的问题。
4.2.3 执行任务
根据收货流程图进行代码实现。
1)controller
/**
* 收货
*
* @param wmsTasksRecords
* @return
*/
@AutoLog(value = "收货")
@Operation(summary="收货")
@RequiresPermissions("inorder:receive_task:addRecords")
@PostMapping(value = "/addRecords")
public Result<String> addRecords(@RequestBody WmsTasksRecords wmsTasksRecords) {
String taskId = wmsTasksRecords.getId();
wmsTasksRecords.setTaskId(taskId);
wmsTasksRecords.setId(null);
wmsTasksService.receive(wmsTasksRecords);
return Result.OK("添加成功!");
}2)service
编写收货方法实现执行任务
/**
* @Description: 任务表
* @Author: jeecg-boot
* @Date: 2025-08-05
* @Version: V1.0
*/
public interface IWmsTasksService extends IService<WmsTasks> {
......
/**
* 收货方法
*/
public void receive(WmsTasksRecords wmsTasksRecords);
/**
* 执行任务
*/
public WmsTasks execute(WmsTasksRecords wmsTasksRecords);
}实现方法:
/**
* 收货
* @param wmsTasksRecords
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void receive(WmsTasksRecords wmsTasksRecords) {
//执行收货任务 向任务表更新收货数量,如果收货完成更新状态为收货完成,记录收货记录
WmsTasks wmsTasks = execute(wmsTasksRecords);
//todo 更新入库单明细中的收货数量及不良品数量,当良品数量加不良品数量等采购数量,更新状态为收货完成
//todo 更新入库单中收货总数量,如果所有明细的状态为收货完成则么入库单的状态为收货完成
//todo 如果入库单收货完成则创建上架任务,根据收货记录创建上架任务
}
/**
* 执行任务
*/
@Transactional(rollbackFor = Exception.class)
public WmsTasks execute(WmsTasksRecords wmsTasksRecords) {
//任务id
String taskId = wmsTasksRecords.getTaskId();
//查询任务信息
WmsTasks wmsTasks = getById(taskId);
//已完成数量
Integer completedQuantity = ObjectUtils.defaultIfNull(wmsTasks.getCompletedQuantity(), 0);
// Integer completedQuantity = wmsTasks.getCompletedQuantity()==null?0:wmsTasks.getCompletedQuantity();
//执行数量
Integer quantity = wmsTasksRecords.getExecQuantity();
if(completedQuantity + quantity > wmsTasks.getQuantity()){
throw new JeecgBootException("执行数量不能大于计划数量");
}
//拷贝执行任务的信息到任务记录对象、商品id,任务号,任务类型
//仓库id
wmsTasksRecords.setTargetWarehouseId(wmsTasks.getTargetWarehouseId());
//入库单id
wmsTasksRecords.setStockInOrderId(wmsTasks.getStockInOrderId());
//入库单明细id
wmsTasksRecords.setStockInOrderItemId(wmsTasks.getStockInOrderItemId());
//波次id 用于波次管理
wmsTasksRecords.setWaveOrderId(wmsTasks.getWaveOrderId());
//波次拣货明细id 用于波次管理
wmsTasksRecords.setWaveSkuSummaryId(wmsTasks.getWaveSkuSummaryId());
//任务id
wmsTasksRecords.setTaskId(wmsTasks.getId());
//商品 id
wmsTasksRecords.setProductId(wmsTasks.getProductId());
//任务编码
wmsTasksRecords.setTaskNumber(wmsTasks.getTaskNumber());
//任务类型
wmsTasksRecords.setTaskType(wmsTasks.getTaskType());
//执行时间
wmsTasksRecords.setOperationTime(new Date());
//执行人
wmsTasksRecords.setOperator(wmsTasks.getOperator());
//添加执行任务记录
boolean save = wmsTasksRecordsService.save(wmsTasksRecords);
if(!save){
throw new JeecgBootException("添加任务执行记录失败!");
}
//更新任务表中的完成数量,新的完成数量为原有完成数量加收货记录的完成数量
// String sql = "update wms_tasks set completed_quantity = completed_quantity + #{ExecQuantity } where id = #{taskId} and completed_quantity <= quantity-#{ExecQuantity }";
LambdaUpdateWrapper<WmsTasks> wmsTasksLambdaUpdateWrapper = new LambdaUpdateWrapper<>();
wmsTasksLambdaUpdateWrapper.setSql("completed_quantity = completed_quantity + "+wmsTasksRecords.getExecQuantity())
.eq(WmsTasks::getId,taskId)
.le(WmsTasks::getCompletedQuantity,wmsTasks.getQuantity()-wmsTasksRecords.getExecQuantity());
int update = getBaseMapper().update(null, wmsTasksLambdaUpdateWrapper);
if(update <= 0){
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.2.4 更新入库单明细
1)service
继续编写收货方法实现更新入库单明细的收货数量及状态。
在入库单明细service中定义方法updateReceivedStatus
/**
* @Description: 入库单明细
* @Author: jeecg-boot
* @Date: 2025-04-16
* @Version: V1.0
*/
public interface IStockInOrderItemsService extends IService<WmsStockInOrderItems> {
....
/**
* 更新收货完成状态
* @param stockInOrderItemId 入库单明细id
*/
void updateReceivedStatus(String stockInOrderItemId);
....实现方法:
/**
* 更新收货完成状态
* @param stockInOrderItemId 入库单明细id
*/
@Override
public void updateReceivedStatus(String stockInOrderItemId){
//查询入库单明细
WmsStockInOrderItems stockInOrderItemUpdate = getById(stockInOrderItemId);
//根据入库单明细id查询任务记录表
LambdaQueryWrapper<WmsTasksRecords> eq = new LambdaQueryWrapper<WmsTasksRecords>().eq(WmsTasksRecords::getStockInOrderItemId, stockInOrderItemId);
List<WmsTasksRecords> wmsTasksRecordsList = wmsTasksRecordsService.list(eq);
//使用stream流汇总wmsTasksRecordsList中的不良品数量
int badQuantity = wmsTasksRecordsList.stream().filter(wmsTasksRecord -> wmsTasksRecord.getInventoryAttribute().equals(WarehouseDictEnum.INVENTORY_ATTRIBUTE_DEFECTIVE.getCode())).mapToInt(WmsTasksRecords::getExecQuantity).sum();
//良品数量
int goodQuantity = wmsTasksRecordsList.stream().filter(wmsTasksRecord -> wmsTasksRecord.getInventoryAttribute().equals(WarehouseDictEnum.INVENTORY_ATTRIBUTE_GOOD.getCode())).mapToInt(WmsTasksRecords::getExecQuantity).sum();
if(badQuantity > 0){
stockInOrderItemUpdate.setDefectiveQuantity(badQuantity);
}
if(goodQuantity > 0){
stockInOrderItemUpdate.setReceivedQuantity(goodQuantity);
}
//如果良品数量加不良品数量等于入库单明细采购数量则更新入库单明细状态为已完成
if(stockInOrderItemUpdate.getExpectedQuantity().equals(stockInOrderItemUpdate.getReceivedQuantity() + stockInOrderItemUpdate.getDefectiveQuantity())){
stockInOrderItemUpdate.setStatus(WarehouseDictEnum.INBOUND_DETAIL_RECEIVED.getCode());
}
updateById(stockInOrderItemUpdate);
}4.2.5 更新入库单
1) service
继续编写收货方法实现更新入库单的收货数量及状态。
在入库单service中定义方法updateReceivedStatus
/**
* @Description: 入库单主表
* @Author: jeecg-boot
* @Date: 2025-04-16
* @Version: V1.0
*/
public interface IStockInOrdersService extends IService<WmsStockInOrders> {
/**
* 更新收货完成状态
* @param stockInOrderId 入库单id
* @return 更新后的入库单状态
*/
String updateReceivedStatus(String stockInOrderId);
...实现类:
/**
* 更新收货完成状态
* @param stockInOrderId 入库单id
*/
public String updateReceivedStatus(String stockInOrderId){
WmsStockInOrders stockInOrdersUpdate = getById(stockInOrderId);
if(stockInOrdersUpdate==null) {
//异常
throw new JeecgBootException("入库单不存在");
}
//当前状态只有是收货中或审核通过时方可更新收货状态
if(!(WarehouseDictEnum.INBOUND_RECEIVING.getCode().equals(stockInOrdersUpdate.getStatus())
|| WarehouseDictEnum.INBOUND_APPROVED.getCode().equals(stockInOrdersUpdate.getStatus()))){
throw new JeecgBootException("非收货中、审核通过状态入库单不允许更新收货状态");
}
//根据入库单id查询所有入库单明细的完成数量总和
List<WmsStockInOrderItems> stockInOrderItems = stockInOrderItemsService.selectByMainId(stockInOrderId);
//使用stream流汇总stockInOrderItems中完成收货数量总和
int totalCompletedQuantity = stockInOrderItems.stream().mapToInt(WmsStockInOrderItems::getReceivedQuantity).sum();
//不良品数量
int totalBadQuantity = stockInOrderItems.stream().mapToInt(WmsStockInOrderItems::getDefectiveQuantity).sum();
// 如果stockInOrderItems中存在一个未收货完成则最终状态为未收货完成,否则为收货完成
boolean b1 = stockInOrderItems.stream().anyMatch(stockInOrderItem -> !stockInOrderItem.getStatus().equals(WarehouseDictEnum.INBOUND_DETAIL_RECEIVED.getCode()));
//新状态
String status = b1 ? WarehouseDictEnum.INBOUND_RECEIVING.getCode() : WarehouseDictEnum.INBOUND_RECEIVED.getCode();
//更新收货总量
stockInOrdersUpdate.setTotalReceivedQuantity(totalCompletedQuantity);
//更新不良品数量
stockInOrdersUpdate.setTotalDefectiveQuantity(totalBadQuantity);
stockInOrdersUpdate.setStatus(status);
updateById(stockInOrdersUpdate);
return status;
}2)完善收货service方法
/**
* 收货
* @param wmsTasksRecords
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void receive(WmsTasksRecords wmsTasksRecords) {
//执行收货任务
WmsTasks wmsTasks = execute(wmsTasksRecords);
//更新入库单明细中的收货数量及不良品数量,当良品数量加不良品数量等采购数量,更新状态为收货完成
stockInOrderItemsService.updateReceivedStatus(wmsTasks.getStockInOrderItemId());
//更新入库单状态为收货完成或收货中
String inOrderStatus = stockInOrdersService.updateReceivedStatus(wmsTasks.getStockInOrderId());
//todo 如果入库单收货完成则创建上架任务,根据收货记录创建上架任务
}3) 测试
测试流程:
创建收货任务
进入待收货列表,点击收货。
一个收货任务可以多次收货。
4.2.6 查询收货记录(实战)
根据需求文档,参考查询任务列表的实现方法实现本功能。
按下边的要求进行开发。
1) mapper接口
public interface WmsTasksRecordsMapper extends BaseMapper<WmsTasksRecords> {
/**
* 任务记录列表
*/
public List<WmsTasksRecords> queryList(WmsTasksRecords wmsTasksRecords);
}修改模型类
在任务记录模型类中添加属性
@Data
@TableName("wms_tasks_records")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@Schema(description="任务表")
public class WmsTasksRecords implements Serializable {
.....
//入库单号
@TableField(exist = false)
private java.lang.String stockInOrderNumber;
//商品名称
@TableField(exist = false)
private java.lang.String productName;
//货主
@TableField(exist = false)
private java.lang.String ownerName;
//仓库名称
@TableField(exist = false)
private java.lang.String warehouseName;
}2)service
public interface IWmsTasksRecordsService extends IService<WmsTasksRecords> {
/**
* 收货记录查询
*/
public IPage<WmsTasksRecords> pageList(WmsTasksRecords wmsTasksRecords,
Integer pageNo,
Integer pageSize);
}3) Controller
修改原有Contorller方法,改调用 新的service方法。
5 库存管理
5.1 库存表设计
收货完成商品也就放入仓库了,在库存表就需要记录商品在仓库的存储情况。
所以,收货完成需要记录库存,上架完成也需要记录库存。
库存表:存储仓库中商品在储位的存放情况,唯一标识:商品ID+储位+批号 全局唯一
库存变更表:存储库存数量变更的记录
库存表、库存变更表结构如下:

阅读需求文档“存储库存”,对库存表结构进行分析:
create table wms_inventory
(
id varchar(36) not null
primary key,
create_by varchar(50) null comment '创建人',
create_time datetime null comment '创建日期',
update_by varchar(50) null comment '更新人',
update_time datetime null comment '更新日期',
sys_org_code varchar(64) null comment '所属部门',
product_id varchar(32) not null comment '商品id',
location_code varchar(32) not null comment '储位编码',
container_code varchar(32) null comment '容器编码',
stock_quantity int not null comment '在库数量',
allocated_quantity int null comment '分配数量',
available_quantity int null comment '可用数量',
batch_number varchar(32) null comment '批号 ',
stock_in_time datetime not null comment '入库时间',
expiry_date date null comment '保质期到期日',
owner_id varchar(32) null comment '货主',
is_sellable varchar(32) null comment '是否可售',
warehouse_id varchar(32) null comment '仓库id',
constraint wms_inventory_unique01
unique (product_id, location_code, batch_number)
)
comment '库存表' collate = utf8mb4_unicode_ci;库存唯一标识:商品ID+储位+批号 全局唯一
增加库存时先根据唯一标识查询库存,如果存在则在更新原有库存记录,否则在库存表增加记录。
在库数量=可用数量+分配数量,例外情况:不良品是不可售的,这种情况: 在库数量>可用数量+分配数量
分析收货、上架、出库时如何计算库存:
- 收货:
良品:
在库数量:增加
可用数量:增加
分配数量:不动
不良品:
在库数量:增加
可用数量:0
分配数量:0
- 上架:(只针对良品)
原储位:
在库数量:减少
可用数量:减少
分配数量:不动
新储位:
在库数量:增加
可用数量:增加
分配数量:不动
- 下单锁定库存(分配库存)
在库数量:不动
可用数量:减少
分配数量:增加
- 拣货 从存储位移动到发货位
原储位:
在库数量:减少
可用数量:不动
分配数量:减少
新储位(发货位):
在库数量:增加
可用数量:0
分配数量:增加
- 发货
拣货储位:
在库数量:减少
可用数量:0
分配数量:减少
5.2 收货存储库存
5.2.1 查询唯一库存
1)生成代码
生成库存表、库存变更表的代码,报名:inventory


将生成的代码中下边的类删除:
删除org.jeecg.modules.wms.inventory.service.impl.WmsInventoryTransServiceImpl
删除org.jeecg.modules.wms.inventory.controller.WmsInventoryTransController

2)查询唯一库存
定义通过库存唯一约束查询库存方法
/**
* @Description: 库存表
* @Author: jeecg-boot
* @Date: 2025-05-13
* @Version: V1.0
*/
public interface IWmsInventoryService extends IService<WmsInventory> {
/**
* 根据唯一键获取库存
* @param productId
* @param locationCode
* @param batchNumber
* @return
*/
public WmsInventory getInventoryByUniqueKey(String productId, String locationCode, String batchNumber);
...
}实现类:
@Service
public class WmsInventoryServiceImpl extends ServiceImpl<WmsInventoryMapper, WmsInventory> implements IWmsInventoryService {
@Override
public WmsInventory getInventoryByUniqueKey(String productId, String locationCode, String batchNumber) {
WmsInventory wmsInventorySource = getOne(new LambdaQueryWrapper<WmsInventory>()
.eq(WmsInventory::getProductId, productId)
.eq(WmsInventory::getLocationCode, locationCode)
.eq(StringUtils.isNotEmpty(batchNumber),WmsInventory::getBatchNumber, batchNumber)
.eq(StringUtils.isEmpty(batchNumber),WmsInventory::getBatchNumber, ""));
return wmsInventorySource;
}
}5.2.2 收货存储库存
1)变更库存接口
接口:
public interface IWmsInventoryTransService extends IService<WmsInventoryTrans> {
/**
* 库存变更
* @param inventoryTransParam
*/
void transfer(WmsInventoryTransParam inventoryTransParam);
}模型类:
此模型类用于操作库存,所以它包括的字段是WmsInventory库存模型类的一部分。
package org.jeecg.modules.wms.inventory.vo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
/**
* @author Mr.M
* @version 1.0
* @description 库存变更参数类
* @date 2025/7/20 18:00
*/
@Data
public class WmsInventoryTransParam {
private static final long serialVersionUID = 1L;
/**商品id*/
@Excel(name = "商品id", width = 15)
@Schema(description = "商品id")
private String productId;
/**执行数量*/
@Excel(name = "执行数量", width = 15)
@Schema(description = "执行数量")
private Integer execQuantity;
/**仓库id*/
@Excel(name = "仓库id", width = 15)
@Schema(description = "仓库id")
private java.lang.String warehouseId;
/**来源储位编码*/
@Excel(name = "来源储位编码", width = 15)
@Schema(description = "来源储位编码")
private String sourceLocationCode;
/**目的储位编码*/
@Excel(name = "目的储位编码", width = 15)
@Schema(description = "目的储位编码")
private String targetLocationCode;
/**batchNumber批次号*/
@Excel(name = "batchNumber批次号", width = 15)
@Schema(description = "batchNumber批次号")
private String batchNumber;
@Excel(name = "保质期", width = 20, format = "yyyy-MM-dd")
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern="yyyy-MM-dd")
private Date expiryDate;
/**备注*/
@Excel(name = "备注", width = 15)
@Schema(description = "备注")
private String remarks;
/**变更类型*/
@Excel(name = "变更类型", width = 15)
@Schema(description = "变更类型")
private String transactionType;
/**执行人*/
@Excel(name = "执行人", width = 15)
@Schema(description = "执行人")
private String operator;
/**执行时间*/
@Excel(name = "执行时间", width = 20, format = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
@Schema(description = "执行时间")
private Date operationTime;
/**是否可售 is_sellable*/
@Excel(name = "是否可售 is_sellable", width = 15)
@Schema(description = "是否可售 is_sellable")
private String isSellable;
}2)收货变更库存
@Service
public class WmsInventoryTransByReceiving extends ServiceImpl<WmsInventoryTransMapper, WmsInventoryTrans> implements IWmsInventoryTransService {
@Autowired
private IWmsInventoryService wmsInventoryService;
//注入储位表service
@Autowired
private IWmsStorageLocationsService wmsStorageLocationsService;
//注入储区service
@Autowired
private IWmsStorageZonesService wmsStorageZonesService;
//注入商品service
@Autowired
private IWmsProductsService wmsProductsService;
@Override
@Transactional(rollbackFor = Exception.class)
public void transfer(WmsInventoryTransParam inventoryTransParam) {
//非空判断,目标储位、商品id、执行数量,是否可售
if (StringUtils.isEmpty(inventoryTransParam.getTargetLocationCode())
|| StringUtils.isEmpty(inventoryTransParam.getProductId())
|| inventoryTransParam.getExecQuantity() == null
|| StringUtils.isEmpty(inventoryTransParam.getIsSellable())) {
throw new RuntimeException("目标储位、商品id、执行数量、是否可售不能为空");
}
//查询商品信息
WmsProducts products = wmsProductsService.getById(inventoryTransParam.getProductId());
if (products == null) {
//抛出异常
throw new RuntimeException("商品id在商品表中不存在");
}
//根据储位编码查询储位 信息
LambdaQueryWrapper<WmsStorageLocations> eq = new LambdaQueryWrapper<WmsStorageLocations>().eq(WmsStorageLocations::getLocationCode, inventoryTransParam.getTargetLocationCode());
WmsStorageLocations wmsStorageLocations = wmsStorageLocationsService.getBaseMapper().selectOne(eq);
if (wmsStorageLocations == null) {
//抛出异常
throw new RuntimeException("储位编码在储位表中不存在");
}
//库存属性 1:可售 2:不可售
String isSellable = inventoryTransParam.getIsSellable();
//根据唯一key获取库存
WmsInventory wmsInventoryDb = wmsInventoryService.getInventoryByUniqueKey(inventoryTransParam.getProductId(), inventoryTransParam.getTargetLocationCode(), inventoryTransParam.getBatchNumber());
//判断库存是否为空
if (wmsInventoryDb == null) {
//创建库存对象
wmsInventoryDb = new WmsInventory();
BeanUtils.copyProperties(inventoryTransParam,wmsInventoryDb);
wmsInventoryDb.setOwnerId(products.getOwnerId());
wmsInventoryDb.setStockQuantity(inventoryTransParam.getExecQuantity());
wmsInventoryDb.setLocationCode(inventoryTransParam.getTargetLocationCode());
//如果储位是可售库存,则设置可用库存为数量
wmsInventoryDb.setAvailableQuantity(isSellable.equals("1")?inventoryTransParam.getExecQuantity():0);
wmsInventoryDb.setAllocatedQuantity(0);
//是否可售
wmsInventoryDb.setIsSellable(isSellable);
//入库时间
wmsInventoryDb.setStockInTime(inventoryTransParam.getOperationTime());
wmsInventoryService.save(wmsInventoryDb);
}else{
LambdaUpdateWrapper<WmsInventory> wmsInventoryLambdaUpdateWrapper = new LambdaUpdateWrapper<WmsInventory>()
.eq(WmsInventory::getId, wmsInventoryDb.getId())
.setSql("stock_quantity=stock_quantity+{0}", inventoryTransParam.getExecQuantity())
.setSql(isSellable.equals("1"),"available_quantity=available_quantity+{0}", inventoryTransParam.getExecQuantity());
wmsInventoryService.update(null,wmsInventoryLambdaUpdateWrapper);
}
}
}5.2.3 测试
1) 修改收货代码
下边在收货方法中增加对库存的操作:
@Service
public class WmsTasksServiceImpl extends ServiceImpl<WmsTasksMapper, WmsTasks> implements IWmsTasksService {
@Autowired
private RedisUtil redisUtil;
@Autowired
private IWmsStockInOrdersService stockInOrdersService;
@Autowired
private IWmsStockInOrderItemsService stockInOrderItemsService;
@Autowired
private IWmsTasksRecordsService wmsTasksRecordsService;
@Autowired
private WmsInventoryTransByReceiving wmsInventoryTransByReceiving;
/**
* 收货
* @param wmsTasksRecords
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void receive(WmsTasksRecords wmsTasksRecords) {
//执行收货任务
WmsTasks wmsTasks = execute(wmsTasksRecords);
//已完成数量
Integer completedQuantity = wmsTasks.getCompletedQuantity();
//将完成数量更新至入库单明细
stockInOrderItemsService.updateReceivedStatus(wmsTasks.getStockInOrderItemId());
//更新入库单状态为收货完成或收货中
String inOrderStatus = stockInOrdersService.updateReceivedStatus(wmsTasks.getStockInOrderId());
//todo 如果入库单收货完成则创建上架任务,根据收货记录创建上架任务
//库存属性
String inventoryAttribute = wmsTasksRecords.getInventoryAttribute();
//向库存表中添加库存记录
// wmsInventoryService.createInventoryByReceive(wmsTasksRecords);
WmsInventoryTransParam inventoryTransParam = new WmsInventoryTransParam();
inventoryTransParam.setProductId(wmsTasksRecords.getProductId());
inventoryTransParam.setExecQuantity(wmsTasksRecords.getExecQuantity());
inventoryTransParam.setWarehouseId(wmsTasksRecords.getTargetWarehouseId());
inventoryTransParam.setTargetLocationCode(wmsTasksRecords.getTargetLocationCode());
inventoryTransParam.setBatchNumber(wmsTasksRecords.getBatchNumber());
inventoryTransParam.setExpiryDate(wmsTasksRecords.getExpiryDate());
inventoryTransParam.setTransactionType(WarehouseDictEnum.INVENTORY_RECEIVING.getCode());
//保质期到期日
inventoryTransParam.setExpiryDate(wmsTasksRecords.getExpiryDate());
//入库时间
inventoryTransParam.setOperationTime(wmsTasksRecords.getOperationTime());
//库存属性为良品则可售
if(WarehouseDictEnum.INVENTORY_ATTRIBUTE_GOOD.getCode().equals(inventoryAttribute)){
inventoryTransParam.setIsSellable("1");
}else{
inventoryTransParam.setIsSellable("0");
}
wmsInventoryTransByReceiving.transfer(inventoryTransParam);
}2)测试流程
创建入库单
审核通过
创建收货任务
收货
观察库存表是否写入正确
5.2.4 阅读其它库存变更类
从课程资料-->源代码-->后端-->库存变更类拷贝其它库存变更类到org.jeecg.modules.wms.inventory.service.impl下
阅读上架、拣货、锁定库存等库存变更类。
5.3 查询库存(实战)
库存查询需要关联查询商品表、货主表、储位表、储区表、仓库表,库存表必须与这些表匹配成功,所以这里采用内链接。
首先阅读需求文档、接口文档,自行编写sql,定义mapper、 service、controller类。
6 Excel导入导出
6.1 导出Excel
1)应用场景
Excel 导入导出技术在数据处理和系统交互中应用广泛。
应用场景:
批量数据录入
- 场景:将外部数据(如调查问卷、订单记录、库存清单)快速导入数据库或业务系统。
- 优势:避免手动逐条输入,提升效率,减少人为错误。
- 案例:电商平台批量导入商品信息;HR 部门导入员工考勤数据。
数据迁移与整合
- 场景:将旧系统数据导出为 Excel,清洗整理后导入新系统。
- 案例:企业更换 ERP 系统时,通过 Excel 过渡数据。
外部数据对接
- 场景:接收合作伙伴或客户提供的 Excel 数据文件,自动解析并存储。
- 案例:银行处理客户批量转账的 Excel 模板文件。
报表生成与分发
- 场景:将系统生成的统计报表(如销售业绩、财务报表)导出为 Excel。
- 优势:便于非技术人员查看、打印或二次加工。
数据备份与归档
- 场景:定期将数据库数据导出为 Excel 作为轻量级备份。
- 注意:适合小规模数据,大规模数据建议用专业备份工具。
跨平台数据共享
- 场景:将数据以 Excel 格式提供给无法直接访问系统的外部人员。
- 案例:向审计部门提供财务数据快照。
数据可视化与演示
- 场景:利用 Excel 的图表功能将原始数据转换为可视化内容。
- 案例:导出销售趋势数据,用于会议演示。
离线数据处理
- 场景:导出数据供用户在无网络环境下编辑,后续再导入系统。
- 案例:销售人员离线修改客户信息后同步回 CRM 系统。
2)主流技术
Apache POI
- 最流行的Java Excel处理库
- 支持.xls(HSSF)和.xlsx(XSSF)格式
- 功能全面但API较底层
EasyExcel(阿里出品)
- 基于POI的封装
- 内存占用低,适合大数据量
- 注解式编程,API简洁
JExcelAPI (JXL)
- 较老的库,只支持.xls格式
- 轻量级但功能有限
OpenCSV
- 专门处理CSV格式
- 简单高效但不支持Excel特有功能
3) EasyExcel
JeecgBoot使用的是Apache POI,本项目使用EasyExcel。
官网地址:https://easyexcel.opensource.alibaba.com/
代码仓库地址:https://gitee.com/easyexcel/easyexcel 或 https://github.com/alibaba/easyexcel
Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。
EasyExcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用EasyExcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便。
官方给了性能测试数据:

4)阅读文档
官网文档:https://easyexcel.opensource.alibaba.com/docs/current/quickstart/write
阅读文档,理解整个写Excel的流程。
- 首先需要数据
private List<DemoData> data() {
List<DemoData> list = ListUtils.newArrayList();
for (int i = 0; i < 10; i++) {
DemoData data = new DemoData();
data.setString("字符串" + i);
data.setDate(new Date());
data.setDoubleData(0.56);
list.add(data);
}
return list;
}- 模型类添加注解
@Getter
@Setter
@EqualsAndHashCode
public class DemoData {
@ExcelProperty("字符串标题")
private String string;
@ExcelProperty("日期标题")
private Date date;
@ExcelProperty("数字标题")
private Double doubleData;
/**
* 忽略这个字段
*/
@ExcelIgnore
private String ignore;
}- 调用API写Excel
/**
* 最简单的写
* <p>
* 1. 创建excel对应的实体对象 参照{@link DemoData}
* <p>
* 2. 直接写即可
*/
@Test
public void simpleWrite() {
// 注意 simpleWrite在数据量不大的情况下可以使用(5000以内,具体也要看实际情况),数据量大参照 重复多次写入
// 写法1 JDK8+
// since: 3.0.0-beta1
String fileName = TestFileUtil.getPath() + "simpleWrite" + System.currentTimeMillis() + ".xlsx";
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
// 如果这里想使用03 则 传入excelType参数即可
EasyExcel.write(fileName, DemoData.class)
.sheet("模板")
.doWrite(() -> {
// 分页查询数据
return data();
});
// 写法2
fileName = TestFileUtil.getPath() + "simpleWrite" + System.currentTimeMillis() + ".xlsx";
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
// 如果这里想使用03 则 传入excelType参数即可
EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data());
// 写法3
fileName = TestFileUtil.getPath() + "simpleWrite" + System.currentTimeMillis() + ".xlsx";
// 这里 需要指定写用哪个class去写
try (ExcelWriter excelWriter = EasyExcel.write(fileName, DemoData.class).build()) {
WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
excelWriter.write(data(), writeSheet);
}1
}通过web方式将Excel下载到客户端
/**
* 文件下载(失败了会返回一个有部分数据的Excel)
* <p>
* 1. 创建excel对应的实体对象 参照{@link DownloadData}
* <p>
* 2. 设置返回的 参数
* <p>
* 3. 直接写,这里注意,finish的时候会自动关闭OutputStream,当然你外面再关闭流问题不大
*/
@GetMapping("download")
public void download(HttpServletResponse response) throws IOException {
// 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream(), DownloadData.class).sheet("模板").doWrite(data());
}5) 添加依赖
在wms 模块添加依赖:
<!--由于项目中用了poi4.1.2,最新版本的easyexcel 4.0.4默认依赖的是poi5.2,这里用easyexcel3.3.4-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.4</version>
<exclusions>
<exclusion>
<artifactId>poi-ooxml-schemas</artifactId>
<groupId>org.apache.poi</groupId>
</exclusion>
</exclusions>
</dependency>6)导出商品
下边我们参考例子代码编写商品导出的测试代码。
- 准备数据
调用商品查询service得到商品列表数据。
- 修改模型类
参考DemoData 类在模型类WmsProducts中添加@ExcelProperty注解,不需要导出哪个列使用@ExcelIgnore注解标识。
示例:

注意选择正确类路径

- 编写导出商品接口
参考上边的示例“通过web方式将Excel下载到客户端”。
修改原有的导出方法:

修改如下:
@RequestMapping(value = "/exportXls")
public void exportXls(HttpServletRequest request,HttpServletResponse response, WmsProducts wmsProducts) throws IOException {
//查询商品列表
Result<IPage<WmsProducts>> iPageResult = queryPageList(wmsProducts, 1, 1000, request);
List<WmsProducts> list = iPageResult.getResult().getRecords();
// 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream(), WmsProducts.class).sheet("模板").doWrite(list);
}7) 测试
最后测试:
进入商品查询列表,点击导出,观察导出数据是否正确。
8)功能完善
按下边的导出字段要求完善商品导出程序

9)思考
我们在导出商品信息只导出了1000条,如果商品数量大于1000怎么办?
你可能想到那就写一个方法查询全部商品,一次性全部导出,这样的话就存在一个问题一次性从数据库查询的数据过多内存占用过大。
请大家思考导出10万商品的方案,导出100万商品的方案。
6.2 导入Excel
1) 阅读文档
官方文档: https://easyexcel.opensource.alibaba.com/docs/current/quickstart/read
- 根据excel列定义模型类
@Getter
@Setter
@EqualsAndHashCode
public class DemoData {
private String string;
private Date date;
private Double doubleData;
}- 定义监听类
读取excel数据时会调用监听类
// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
@Slf4j
public class DemoDataListener implements ReadListener<DemoData> {
/**
* 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 100;
/**
* 缓存的数据
*/
private List<DemoData> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
/**
* 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
*/
private DemoDAO demoDAO;
public DemoDataListener() {
// 这里是demo,所以随便new一个。实际使用如果到了spring,请使用下面的有参构造函数
demoDAO = new DemoDAO();
}
/**
* 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
*
* @param demoDAO
*/
public DemoDataListener(DemoDAO demoDAO) {
this.demoDAO = demoDAO;
}
/**
* 这!解析都会来调用
*
* @param data one row value. Is is same as {@link AnalysisContext#readRowHolder()}
* @param context
*/
@Override
public void!e(DemoData data, AnalysisContext context) {
log.info("解析到一条数据:{}", JSON.toJSONString(data));
cachedDataList.add(data);
// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
if (cachedDataList.size() >= BATCH_COUNT) {
saveData();
// 存储完成清理 list
cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
}
}
/**
* 所有数据解析完成了 都会来调用
*
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 这里也要保存数据,确保最后遗留的数据也存储到数据库
saveData();
log.info("所有数据解析完成!");
}
/**
* 加上存储数据库
*/
private void saveData() {
log.info("{}条数据,开始存储数据库!", cachedDataList.size());
demoDAO.save(cachedDataList);
log.info("存储数据库成功!");
}
}- 持久层
/**
* 假设这个是你的DAO存储。当然还要这个类让spring管理,当然你不用需要存储,也不需要这个类。
**/
public class DemoDAO {
public void save(List<DemoData> list) {
// 如果是mybatis,尽量别直接调用多次insert,自己写一个mapper里面新增一个方法batchInsert,所有数据一次性插入
}
}- 读取excel
/**
* 最简单的读
* <p>
* 1. 创建excel对应的实体对象 参照{@link DemoData}
* <p>
* 2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link DemoDataListener}
* <p>
* 3. 直接读即可
*/
@Test
public void simpleRead() {
// 写法1:JDK8+ ,不用额外写一个DemoDataListener
// since: 3.0.0-beta1
String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
// 这里默认每次会读取100条数据 然后返回过来 直接调用使用数据就行
// 具体需要返回多少行可以在`PageReadListener`的构造函数设置
EasyExcel.read(fileName, DemoData.class, new PageReadListener<DemoData>(dataList -> {
for (DemoData demoData : dataList) {
log.info("读取到一条数据{}", JSON.toJSONString(demoData));
}
})).sheet().doRead();
// 写法2:
// 匿名内部类 不用额外写一个DemoDataListener
fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
// 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
EasyExcel.read(fileName, DemoData.class, new ReadListener<DemoData>() {
/**
* 单次缓存的数据量
*/
public static final int BATCH_COUNT = 100;
/**
*临时存储
*/
private List<DemoData> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
@Override
public void invoke(DemoData data, AnalysisContext context) {
cachedDataList.add(data);
if (cachedDataList.size() >= BATCH_COUNT) {
saveData();
// 存储完成清理 list
cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
saveData();
}
/**
* 加上存储数据库
*/
private void saveData() {
log.info("{}条数据,开始存储数据库!", cachedDataList.size());
log.info("存储数据库成功!");
}
}).sheet().doRead();
// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
// 写法3:
fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
// 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead();
// 写法4
fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
// 一个文件一个reader
try (ExcelReader excelReader = EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).build()) {
// 构建一个sheet 这里可以指定名字或者no
ReadSheet readSheet = EasyExcel.readSheet(0).build();
// 读取一个sheet
excelReader.read(readSheet);
}
}- web上传文件读取excel
/**
* 文件上传
* <p>
* 1. 创建excel对应的实体对象 参照{@link UploadData}
* <p>
* 2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link UploadDataListener}
* <p>
* 3. 直接读即可
*/
@PostMapping("upload")
@ResponseBody
public String upload(MultipartFile file) throws IOException {
EasyExcel.read(file.getInputStream(), UploadData.class, new UploadDataListener(uploadDAO)).sheet().doRead();
return "success";
}2) 导入商品
下边根据示例实现导入商品。
- 定义模型类
根据excel编写模型类,模型类的属性与excel 列 从左到右保持一致。
excel列:

模型类:
@ExcelProperty注解中的名称也Excel列名保持一致。
package org.jeecg.modules.wms.goods.excel;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* @Description: 商品导入模型类
* @Author: Mr.M
* @Date: 2025-08-05
* @Version: V1.0
*/
@Data
public class WmsProductsImport implements Serializable {
private static final long serialVersionUID = 1L;
/**商品名称*/
@ExcelProperty("商品名称")
private String productName;
/**商品编码*/
@ExcelProperty("sku编码")
private String productCode;
/**商品条码*/
@ExcelProperty("商品条码")
private String productBarcode;
@ExcelProperty("商品规格")
private String productSpec;
@ExcelProperty("商品批次")
private String productBatch;
@ExcelProperty("供应商条码")
private String supplierBarcode;
/**宽*/
@ExcelProperty("宽")
private Double width;
/**长*/
@ExcelProperty("长")
private Double length;
/**高*/
@ExcelProperty("高")
private Double height;
/**体积*/
@ExcelProperty("体积")
private Double volume;
/**毛重*/
@ExcelProperty("毛重")
private Double grossWeight;
/**净重*/
@ExcelProperty("净重")
private Double netWeight;
/**包装规格*/
@ExcelProperty("包装规格")
private String packagingSpec;
/**养护周期(天)*/
@ExcelProperty("养护周期(天)")
private Integer maintenanceCycle;
/**保质期(天)*/
@ExcelProperty("保质期(天)")
private Integer shelfLife;
/**计量单位*/
@ExcelProperty("计量单位")
private String unit;
/**是否保质期管控*/
@ExcelProperty("是否保质期管控")
private Integer isExpiryControlled;
/**状态*/
@ExcelProperty("状态")
private String status;
//货主名称
@ExcelProperty("货主名称")
private String ownerName;
//分类名称
@ExcelProperty("商品分类")
private String categoryName;
@ExcelProperty("商品品牌")
private String productBrandName;
}- 定义监听类
package org.jeecg.modules.wms.goods.excel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.util.ListUtils;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.modules.wms.goods.entity.WmsProducts;
import org.jeecg.modules.wms.goods.service.IWmsProductsService;
import org.jeecg.modules.wms.goods.service.impl.WmsProductsServiceImpl;
import org.springframework.beans.BeanUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
@Slf4j
public class ImportGoodsListener implements ReadListener<WmsProductsImport> {
/**
* 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 100;
/**
* 缓存的数据
*/
private List<WmsProductsImport> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
/**
* 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
*/
private IWmsProductsService wmsProductsService;
/**
* 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
*
* @param wmsProductsService
*/
public ImportGoodsListener(IWmsProductsService wmsProductsService) {
this.wmsProductsService = wmsProductsService;
}
/**
* 这个每一条数据解析都会来调用
*
* @param data one row value. Is is same as {@link AnalysisContext#readRowHolder()}
* @param context
*/
@Override
public void invoke(WmsProductsImport data, AnalysisContext context) {
log.info("解析到一条数据:{}", JSON.toJSONString(data));
cachedDataList.add(data);
// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
if (cachedDataList.size() >= BATCH_COUNT) {
saveData();
// 存储完成清理 list
cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
}
}
/**
* 所有数据解析完成了 都会来调用
*
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 这里也要保存数据,确保最后遗留的数据也存储到数据库
saveData();
log.info("所有数据解析完成!");
}
/**
* 加上存储数据库
*/
private void saveData() {
log.info("{}条数据,开始存储数据库!", cachedDataList.size());
wmsProductsService.importProduct(cachedDataList);
log.info("存储数据库成功!");
}
}在IWmsProductsService中定义如下接口:
/**
* 导入商品
* @param wmsProductsImportList
*/
void importProduct(List<WmsProductsImport> wmsProductsImportList);实现类:
package org.jeecg.modules.wms.goods.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.modules.wms.goods.entity.*;
import org.jeecg.modules.wms.goods.excel.WmsProductsImport;
import org.jeecg.modules.wms.goods.mapper.WmsProductsMapper;
import org.jeecg.modules.wms.goods.service.*;
import org.springframework.beans.BeanUtils;
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.List;
/**
* @Description: 商品信息表
* @Author: jeecg-boot
* @Date: 2025-04-14
* @Version: V1.0
*/
@Service
public class WmsProductsServiceImpl extends ServiceImpl<WmsProductsMapper, WmsProducts> implements IWmsProductsService {
....
/**
* 导入商品
* @param wmsProductsImportList
*/
@Override
public void importProduct(List<WmsProductsImport> wmsProductsImportList) {
for (WmsProductsImport wmsProductsImport : wmsProductsImportList){
WmsProducts products = convertWmsProducts(wmsProductsImport);
add(products);
}
}
/**
* 将WmsProductsImport转换为WmsProducts
* @param wmsProductsImport
* @return WmsProducts
*/
private WmsProducts convertWmsProducts(WmsProductsImport wmsProductsImport) {
WmsProducts wmsProducts = new WmsProducts();
BeanUtils.copyProperties(wmsProductsImport,wmsProducts);
//处理名称
//货主名称
String ownerName = wmsProductsImport.getOwnerName();
//根据货主名称查询货主
LambdaQueryWrapper<WmsCargoOwners> eq = new LambdaQueryWrapper<WmsCargoOwners>()
.eq(WmsCargoOwners::getOwnerName, ownerName);
WmsCargoOwners wmsCargoOwners = wmsCargoOwnersService.getOne(eq);
wmsProducts.setOwnerId(wmsCargoOwners.getId());
//根据商品分类名称查询分类
String categoryName = wmsProductsImport.getCategoryName();
LambdaQueryWrapper<WmsProductCategories> queryWrapper = new LambdaQueryWrapper<WmsProductCategories>()
.eq(WmsProductCategories::getCategoryName, categoryName);
WmsProductCategories wmsProductCategories = wmsProductCategoriesService.getOne(queryWrapper);
wmsProducts.setCategoryId(wmsProductCategories.getId());
//根据品牌名称查询 品牌
String brandName = wmsProductsImport.getProductBrandName();
LambdaQueryWrapper<WmsProductBrand> queryWrapper2 = new LambdaQueryWrapper<WmsProductBrand>()
.eq(WmsProductBrand::getName, brandName);
WmsProductBrand wmsProductBrand = wmsProductBrandService.getOne(queryWrapper2);
wmsProducts.setProductBrand(wmsProductBrand.getId());
return wmsProducts;
}
....- 定义导入接口
/**
* 通过excel导入数据
*
* @param file
* @return
*/
@RequiresPermissions("goods:wms_products:importExcel")
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(MultipartFile file) throws IOException {
EasyExcel.read(file.getInputStream(), WmsProductsImport.class, new ImportGoodsListener(wmsProductsService)).sheet().doRead();
return Result.OK("文件导入成功!");
}3) 测试
进入商品列表,导入商品。
4)思考
如何给用户提示导入成功个数、失败个数及失败原因?
组内讨论,拿出技术方案。
6.3 导出库存(实战)
学完第七节的内容后,阅读需求文档,完成导出库存功能。
6.4 导入库存(实战)
学完第七节的内容后,阅读需求文档,完成导入库存功能。
7 web打印
7.1 web打印技术Lodop
1)应用场景
Web打印技术在现代Web应用中扮演着重要角色,尤其在需要将数字内容快速转化为纸质文档或特定格式输出的场景中。
应用场景:
- 企业级应用
报表与单据打印
- 场景:ERP、财务系统、仓储管理系统中的订单、发票、发货单、财务报表等。
- 需求:高精度排版、批量打印、支持条形码/二维码(如物流单)。
合同与协议
- 场景:电子签约后直接打印具有法律效力的合同,或生成多联单据。
- 需求:固定格式、签名位置保留、防篡改(如通过PDF数字签名)。
- 公共服务与政务
自助终端打印
- 场景:医院挂号单、政府办事回执、银行流水、机场登机牌。
- 需求:快速响应、无需预览、兼容热敏打印机等特殊设备。
证件与表单
- 场景:在线填写的申请表(如签证、营业执照)需标准化打印。
- 需求:严格对齐字段、A4/特定纸张尺寸适配。
- 电商与物流
电子面单与标签
- 场景:电商订单、物流货运标签、仓库货架标签。
- 需求:动态填充数据、小尺寸纸张(如10x15cm)、热敏打印兼容。
购物凭证
- 场景:线上消费后的电子收据(如外卖小票)。
- 挑战:自动截断长列表、分页控制。
- 教育与办公
在线试卷与作业
- 场景:教育平台生成可打印的试题或学习资料。
- 需求:分页避免题目被切断、页眉页脚标注页码。
协同文档打印
- 场景:Google Docs、Notion等在线工具的内容导出为纸质版。
- 挑战:复杂布局(如分栏、表格)的打印保真度。
- 医疗与健康
检查报告与处方
- 场景:医院系统输出的化验单、处方笺。
- 需求:高可读性、关键数据突出显示(如红色警示值)。
2)主流技术
- 浏览器原生打印(基础方案)
技术:
- 调用浏览器API:
window.print()。 - 使用CSS打印样式控制布局(
@page、page-break-before等)。 - 基于js封装的组件sv-print,带打印设计器。
- 调用浏览器API:
优点:
- 无需依赖第三方库,兼容所有现代浏览器。
- 用户可自主选择打印机和设置(如纸张方向、页边距)。
缺点:
- 样式控制有限(部分浏览器忽略某些CSS属性)。
- 无法静默打印,必须弹出预览对话框。
适用场景:
- 简单的页面打印(如文章、表格);
- 对打印格式要求不高的场景。
- 服务端生成PDF/Excel后打印
技术:
服务端工具:
- Node.js:
pdfkit、puppeteer - Java:
iText、Apache PDFBox、EasyExcel、Apache POI。
- Node.js:
流程:前端请求 → 服务端生成PDF /Excel → 返回下载链接或直接推送打印机。
优点:
- 精准控制排版(支持复杂图表、字体嵌入)。
- 避免浏览器兼容性问题,支持静默打印(需配合后台服务)。
缺点:
- 服务端资源消耗高,动态内容需频繁生成PDF。
适用场景:
- 高精度报表(如财务报告);
- 需要批量或定时打印的任务(如每日对账单)。
- 专用打印协议与插件
技术:
- 指令集:ZPL(斑马打印机)、ESC/POS(热敏小票打印机)。
- 插件:ActiveX(已淘汰)、NPAPI(仅旧版浏览器)、Lodop(当前流行)。
优点:
- 直接控制打印机(如切纸、黑标定位)。
缺点:
- 依赖特定环境(如企业内网、客户端安装)。
适用场景:
- 物流面单、零售小票等专用设备打印。
- 云打印服务
技术:
- Google Cloud Print(已停用)、第三方API(如PrintNode),C-Lodop
- 原理:通过云端队列管理打印任务,支持远程设备。
优点:
- 跨地域打印(如总部下发任务至分店打印机)。
缺点:
- 依赖网络,有数据隐私风险。
3)Lodop
Lodop(标音:劳道谱,俗称:露肚皮)是专业WEB控件,用它既可裁剪输出页面内容,又可用程序代码直接实现复杂打印。
官网:https://www.lodop.net/index.html
Lodop的核心特点
精准控制能力:
- 直接调用打印机底层功能(如切纸、纸张类型、多联打印)。
- 支持套打(定位到毫米级,如发票、支票的预印刷表单填充)。
多平台适配:
- 通过插件(Windows)或云打印服务(跨平台扩展)实现。
- 兼容IE(ActiveX)、Chrome(NPAPI/扩展)、Edge等浏览器。
模板化设计:
- 提供可视化设计器,支持保存打印模板(
.lod格式),动态绑定数据。
- 提供可视化设计器,支持保存打印模板(
静默打印:
- 无需用户交互即可后台打印(需权限配置)。
在线示例:https://www.lodop.net/LodopDemo.html
通过在线示例去了解Lodop强大的功能。
运行示例需要下载安装Lodop,地址:http://www.c-lodop.com/download.html
提供两类安装文件:
1.Lodop打印控件安装文件两个:
32位安装程序:install_lodop32.exe;
64位安装程序:install_lodop64.exe;
2.C-Lodop云打印服务安装文件俩个,X.XXX表示版本号:
32位安装程序CLodop_Setup_for_Win32NT_X.XXX.exe;
64位安装程序CLodop_Setup_for_Win64NT_X.XXX.exe.
功能方式区别
1.Lodop打印控件是浏览器插件,与浏览器紧密结合来实现本地打印,典型用法可以实现样例19、36这种内嵌到浏览器里交互方式。
2.C-Lodop打印服务是以提供服务方式解决web打印,摆脱了对浏览器的依赖,解决了新版谷歌(45版及后续版本)、64位火狐、edge等浏览器不再支持Lodop插件方式的情况,C-Lodop打印服务支持所有浏览器。
我们下载 C-Lodop,它既可以作为打印控件也可以作为打印服务。

安装完成后可以运行示例 :https://www.lodop.net/LodopDemo.html
7.2 web打印技术svp
1)sv-print打印设计器
sv-print是一个基于js封装的组件sv-print,带打印设计器,可以用于其他UI库/框架,如 React、Vue 和 Angular。当然也可在原生 html 中使用,比如: jQuery 项目,sv-print 依赖Lodop。
官网:https://www.ibujian.cn/svp/
自带打印设计器:

本项目前端已经安装了sv-print的依赖并实现的打印设计器,使用管理员登录项目访问下边的菜单

实现打印设计器(了解)
官方文档提供了实现打印设计器的方法,操作文档地址:https://www.ibujian.cn/svp/a01.html#引入打印样式

代码如下:
<template>
<Designer :template="template" @onDesigned="onDesigned" />
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { Designer } from '@sv-print/vue3';
let template = ref({});
const onDesigned = (e) => {
console.log('onDesigned');
console.log(e);
const { hiprint, designerUtils } = e.detail;
console.log(hiprint);
console.log(designerUtils);
console.log(designerUtils.printTemplate);
};
</script>在views/demo/print下定义svdesigner.vue,内容复制上边官网的代码。

在main.ts/main.js 文件中引入组件样式
import { createApp } from 'vue';
import App from './App.vue';
// 引入组件样式
import 'sv-print/dist/style.css';
createApp(App).mount('#app');引入后如下所示:

下边配置菜单

为管理员分配权限“打印设计器”

保存后刷新前端页面,设计器可以正常显示

测试打印设计器
下边测试打印设计器,从“views\printTemplate\printTemplate”找到inorder-panel.js ,此文件是入库单打印模板,打开此文件,除了“export default” 以外的内容全部复制。

鼠标移动到上图中“保存”,点击“编辑模板数据”,粘贴刚才复制的内容。

点击“确定”此时发现打印设计器出现的入库单的模板样式

点击右上角的“预览”


注意:在设计器中如果二维码指定了字段名在预览时不显示,这个在稍后我们加载模板数据即可显示。
在右侧选择打印机即可打印。
以上演示的是通过web打印的效果,具体打印的效果请阅读需求文档“入库单打印”功能用例。
稍后学习打印模板的制作并在入库单管理程序中使用模板进行打印。
2)制作打印模板
进入打印设计器,通过拖拉方式制作打印模板。
首先清楚模板中几个元素的作用:文档:https://www.ibujian.cn/svp/a03.html

默认的元素控件只有
- 文本 (可设置参数,渲染为二维码,条形码)
- 图片
- 长文本 (内容超过面板高度时,会自动分页)
- 表格 (内容超过面板高度时,会自动分页)
- html
- 横线
- 竖线
- 矩形
- 椭圆
文本控件示例
文本控件需要输入文本内容和字段名,字段名用于填充数据。

表格控件示例
表格控件首先需要设计表格整体的字段名。

然后再设计每个列的名称和字段名

二维码控件示例
二维码控件需要设置标题、字段名、测试数据

制作新模板
下边我们参考入库单打印模板,自成制作一个新的模板,原成果请参考 views\printTemplate\printTemplate\inorder-panel.js
制作过程中及时点击右上角的“保存”按钮将模板保存到本地,并将文件导出到本地,当遇到浏览器刷新后可以将本地的模板导入打印设计器。

3)打印测试
填充数据格式
制作完成通过“编辑模板数据”获得打印模板的内容,打印模板代码是json数据格式。
下边先了解模板的json格式,再找到模板中的数据字段。
文档:https://www.ibujian.cn/svp/a02.html#模板json格式
模板可以包括多个面板,在panels[]中定义
每个面板中的打印元素定义在printElements[]中,如下:
{
// 面板, 支持多面板
panels: [
{
index: 0, // 下标
name: '面板0',
height: 297, // 面板高度: 单位mm
width: 210, // 面板宽度: 单位mm
paperHeader: 49.5, // 页眉线: 单位pt, 以上的元素,每页显示
paperFooter: 780, // 页脚线: 单位pt, 以下的元素,每页显示
// 打印元素信息
printElements: [
{
options: {
left: 439.5, // 元素 相对当前 面板 的左边距: 单位pt
top: 10.5, // 元素 相对当前 面板 的上边距: 单位pt
height: 33, // 元素的高度: 单位pt
width: 150, // 元素的宽度: 单位pt
title: '支持Vue、Vue3、React、jQuery', // 元素的标题
fontSize: 12, // 元素的字体大小: 单位pt
..., // 其他元素的属性, 支持扩展
},
printElementType: {
title: '文本', // 拖拽元素时显示的标题 ,options.title 没有时,显示的标题.
// 元素的类型, 默认内置的类型: text, image, longText, table, html, hline, vline, rect, oval
type: 'text',
},
},
],
paperNumberLeft: 565.5, // 页码 相对当前 面板 的左边距: 单位pt
paperNumberTop: 819, // 页码 相对当前 面板 的上边距: 单位pt
paperNumberContinue: true, // 多面板/模板, 是否页码续排
paperNumberDisabled: true, // 不显示页码
paperNumberFormat: '${paperNo}-${paperCount}', // 页码格式
backgroundColor: '#f1ebff', // 面板的背景颜色
orient: 1, // 打印方向(仅自定义纸张大小): 1-纵向, 2-横向
overPrintOptions: { // 套打功能
content: '', // 套打背景图
opacity: 0.7, // 透明度
type: 1, // 套打类型: 1-仅设计, 2-同时打印
},
watermarkOptions: { // 水印配置
content: 'sv-print', // 水印内容
fillStyle: 'rgba(87, 13, 248, 0.5)', // 水印颜色
fontSize: '36px', // 水印字体大小
rotate: 25, // 水印旋转角度
width: 413, // 水印宽度
height: 310, // 水印高度
timestamp: true, // 是否显示时间戳
format: 'YYYY-MM-DD HH:mm', // 时间戳格式
},
// 排列填充纸张
panelLayoutOptions: {
layoutType: 'column', // 排列方式: column-纵向, row-横向
layoutRowGap: 0, // 垂直间距 单位mm
layoutColumnGap: 0, // 水平间距 单位mm
},
},
{
index: 1,
name: '面板1',
height: 297,
width: 210,
paperHeader: 49.5,
paperFooter: 780,
printElements: [],
}
}注意 模板中面板的宽高单位是 mm, 元素和其他参数的的单位是 pt.
PT(Point)是印刷和排版领域常用的绝对长度单位,1pt定义为1/72英寸。
- 1英寸 = 25.4毫米。
- 1pt = 25.4mm ÷ 72 ≈ 0.35146mm
以入库单打印模板为例,从模板中找到每个数据对应的字段,如下:


以上是单个数据,下边是表格的field和每列的field
表格的field: table
序号:orderIndex
商品编码:productCode


找到所有field后就可以准备模板对应的数据,和上边模板对应的数据示例如下,数据是json格式,每个key对应模板中的field。
{
"orderNumber": "ASN202505140002",
"warehouseName": "中原一号仓库",
"ownerName": "传智教育",
"sourceNumber": "S00293932",
"warehouseManager": "小王",
"editor": "小李",
"url": "http://www.itcast.cn",
"table": [
{
"orderIndex": "1",
"productCode": "1010201",
"productName": "刹车片",
"expectedQuantity": "101",
"remarks": ""
}
,
{
"orderIndex": "2",
"productCode": "1010202",
"productName": "刹车片2",
"expectedQuantity": "33",
"remarks": ""
}
]
}编写测试页面
有了模板和数据就可以进行打印测试。
参考文档(https://www.ibujian.cn/svp/api/a01.html#hiprint)定义一个测试页面:
在views/demo/print下创建printtest.vue
<template>
<a-button type="primary" @click="printWeb">打印</a-button>
</template>
<script setup lang="ts">
import { getLodop } from '/@/assets/LodopFuncs' //导入模块
import { hiprint } from 'sv-print';
import inorderPanel from "@/views/printTemplate/inorder-panel";
import {ref} from 'vue';
const printData=ref({});
printData.value = {
"orderNumber": "ASN202505140002",
"warehouseName": "中原一号仓库",
"ownerName": "传智教育",
"sourceNumber": "S00293932",
"warehouseManager": "小王",
"editor": "小李",
"url": "http://www.itcast.cn",
"table": [
{
"orderIndex": "1",
"productCode": "1010201",
"productName": "刹车片",
"expectedQuantity": "101",
"remarks": ""
}
,
{
"orderIndex": "2",
"productCode": "1010202",
"productName": "刹车片2",
"expectedQuantity": "33",
"remarks": ""
}
]
}
function printWeb() {
// 创建模板对象
const hiprintTemplate = new hiprint.PrintTemplate({
template: inorderPanel, // 模板json对象
});
const jqueryObj = hiprintTemplate.getHtml(printData.value);
const html = jqueryObj.html();
console.log(html)
// const template = hiprintTemplate.getJson();
hiprintTemplate.print(printData.value, {}, {
styleHandler: () => {
return '<link href="http://xx/print-lock.css" media="print" rel="stylesheet">'
}
})
}
</script>打印时需要通过http引用CSS样式,从前端工程找到"public/print-lock.css",并将此文件上传到OSS上:
通过菜单进入对象存储页面

进入OSS上传文件界面

将print-lock.css文件上传到OSS,如下图:

右键print-lock.css的文件地址,点击“检查”,找到此文件的完整路径

将“print-lock.css”的路径 覆盖printtest.vue中的“http://xx/print-lock.css”。
配置菜单
要想测试刚才编写的页面需要配置菜单。

将菜单分配给管理员:

打印测试
刷新页面,通过菜单“打印测试”进入测试页面,点击“打印”,如下图:

选择“目标打印机”,点击“打印”即可将预览的内容在打印机打印。
7.3 入库单打印
1)准备模板数据
刚才已经完成了打印测试,我们只需要将测试数据改为入库单的真实数据即可。
入库单打印流程如下:
进入入库单列表
点击“打印”,打开打印窗口,将入库单信息从父窗口传递到子窗口
进入打印窗口,请求后端根据入库单查询入库单明细列表
将入库单数据和入库单明细数据格式化为与模板匹配的数据。
用户扫描入库单的二维码可以查询入库单的基本信息。(暂不实现)
- 修改查询入库单明细接口
package org.jeecg.modules.wms.inorder.controller;
import java.io.UnsupportedEncodingException;
import java.io.IOException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.HashMap;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jeecg.modules.wms.goods.entity.WmsCargoOwners;
import org.jeecg.modules.wms.goods.service.IWmsCargoOwnersService;
import org.jeecg.modules.wms.warehouse.entity.WmsWarehouses;
import org.jeecg.modules.wms.warehouse.service.IWmsWarehousesService;
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.system.query.QueryRuleEnum;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.wms.inorder.entity.WmsStockInOrderItems;
import org.jeecg.modules.wms.inorder.entity.WmsStockInOrders;
import org.jeecg.modules.wms.inorder.vo.WmsStockInOrdersPage;
import org.jeecg.modules.wms.inorder.service.IWmsStockInOrdersService;
import org.jeecg.modules.wms.inorder.service.IWmsStockInOrderItemsService;
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 com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import com.alibaba.fastjson.JSON;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.apache.shiro.authz.annotation.RequiresPermissions;
/**
* @Description: 入库单主表
* @Author: jeecg-boot
* @Date: 2025-09-03
* @Version: V1.0
*/
@Tag(name="入库单主表")
@RestController
@RequestMapping("/inorder/wmsStockInOrders")
@Slf4j
public class WmsStockInOrdersController {
.....
@Operation(summary="入库单明细-通主表ID查询")
@GetMapping(value = "/queryWmsStockInOrderItemsByMainId")
public Result<IPage<WmsStockInOrderItems>> queryWmsStockInOrderItemsListByMainId(@RequestParam(name="id",required=true) String id) {
List<WmsStockInOrderItems> wmsStockInOrderItemsList = wmsStockInOrderItemsService.selectByMainId(id);
return Result.OK(wmsStockInOrderItemsList );
}
...- 修改\wms\inorder\mapper\xml\WmsStockInOrderItemsMapper.xmlmapper.xml,完善sql
<select id="selectByMainId" parameterType="java.lang.String" resultType="org.jeecg.modules.wms.inorder.entity.WmsStockInOrderItems">
SELECT a.*,wp.product_code,wp.product_name
FROM wms_stock_in_order_items a left join wms_products wp on a.product_id = wp.id
WHERE
a.order_id = #{mainId} </select>- 修改入库单明细模型类,添加属性与sql字段对应
@Schema(description="入库单明细")
@Data
@TableName("wms_stock_in_order_items")
public class WmsStockInOrderItems implements Serializable {
....
//商品名称
@TableField(exist = false)
private String productName;
//商品编码
@TableField(exist = false)
private String productCode;
}2)入库单打印
打开入库单打印页面WmsStockInOrdersPrintModal.vue,阅读页面的源代码。

提供两种打印方式:web打印和Lodop打印。
两种方法实质上都是使用的Lodop打印,这里处于学习的考虑提供两种方法。
下边是Lodop的打印效果
选择 Lodop打印

效果:

7.4 商品条码打印
1)认识条码
条形码(Barcode)是一种通过图形排列编码信息的机器可读技术,广泛应用于商品、物流、医疗、工业等领域。
条形码的分类
- 一维条形码(1D Barcode)
- EAN-13:国际通用的商品条码(如商品包装上的13位数字)。
- UPC-A:北美常用的12位商品条码(如超市商品)。
- Code 128:高密度编码,支持字母、数字和符号(物流、仓储常用)。
- Code 39:支持数字和大写字母(工业、医疗设备常用)。
- ITF-14:用于外箱包装的14位物流条码。
- Codabar:用于图书馆、血库等场景。
- 二维条形码(2D Barcode)
- QR Code:快速响应矩阵码,存储量大(如支付、广告)。
- Data Matrix:小面积高密度编码(电子元件、药品追溯)。
- PDF417:堆叠式条码,可存储大量数据(身份证、登机牌)。
- Aztec Code:中心定位图案,适用于票据(火车票、机票)。
常用条形码
- 零售商品:EAN-13、UPC-A
- 物流包装:ITF-14、Code 128
- 工业场景:Code 39
- 移动应用:QR Code
- 证件存储:PDF417
本项目使用Code128码,CODE128码是广泛应用在企业内部管理、生产流程、物流控制系统方面的条码码制,由于其优良的特性在管理信息系统的设计中被广泛使用,CODE128码是应用最广泛的条码码制之一。
CODE128 码可表示从 ASCII 0 到ASCII 127 共128个字符,故称128码。其中包含了数字、字母和符号字符。
●有三种不同的版本:A(数字、大写字母、控制字符)B(数字、大小字母、字符)C(双位数字)
CODE128A:标准数字和大写字母,控制符,特殊字符
CODE128B:标准数字和大写字母,小写字母,特殊字符
CODE128C:[00]-[99]的数字对集合,共100个
Code 128码与Code 39码有很多的相近性,都广泛运用在企业内部管理、生产流程、物流控制系统方面。不同的在于Code 128比Code 39能表现更多的字符,单位长度里的编码密度更高。当单位长度里不能容下Code 39编码或编码字符超出了Code 39的限制时,就可选择Code 128来编码。所以Code 128比Code 39更具灵活性。
生成原理
编码规则
- 根据条码类型将数据(如数字、字母)转换为对应的二进制序列。
- 例如:EAN-13中,每位数字对应一组黑白条纹的排列(如“1”编码为
0001101)。
图形化生成
- 通过软件将二进制序列转换为黑白条纹(一维码)或点阵(二维码)。
- 添加起始符、终止符和校验位(如EAN-13的最后一位是校验码)。
校验机制
- 多数条码包含校验位,防止扫描错误(如Code 128使用模103校验)。
识别原理
光学扫描
- 一维码:激光或LED扫描仪检测黑白条纹的反射光差异,转换为电信号。
- 二维码:摄像头拍摄图像,通过算法定位角标记(如QR Code的三个方框)。
解码过程
- 图像处理:二值化(黑白转换)、降噪、矫正畸变。
- 解码算法:根据条码类型解析条纹宽度或点阵排列,还原原始数据。
- 校验验证:检查校验位确保数据准确性。
输出结果
- 解码后的数据传递至计算机系统(如POS机显示价格,扫码支付跳转链接)
2) 制作打印模板

条码使用Code128B格式。

成果参考前端工程“view/printTemplate/productbarcode-panel.js”文件。
3)打印页面
下边的内容在页面已实现,了解即可。
前端页面 "view/goods/WmsProductsList.vue" 中打印条码按钮:
<a-button type="primary" v-auth="'goods:wms_products:exportXls'" preIcon="ant-design:export-outlined" @click="print"> 打印条码</a-button>需要在页面导入依赖:
import {hiprint} from "sv-print";
import productbarcodePanel from "@/views/printTemplate/productbarcode-panel";编写打印方法
//获取选择的商品
function getSelectedProducts() {
let printDataArray = []
for(let i=0;i<rowSelection.selectedRows.length;i++){
console.log(rowSelection.selectedRows[i])
printDataArray.push(rowSelection.selectedRows[i])
}
return printDataArray;
}
//web打印
function print() {
let printDataArray = getSelectedProducts();
console.log(printDataArray)
// 创建模板对象
const hiprintTemplate = new hiprint.PrintTemplate({
template: productbarcodePanel, // 模板json对象
});
hiprintTemplate.print(printDataArray, {}, {
styleHandler: () => {
return '<link href="http://xx/print-lock.css" media="print" rel="stylesheet">'
}
})
}4) 测试效果
测试时需要设置纸张大小:
选择打印机:

上图设置的是传真,如果不显示可以在“控制面板-->程序-->启动或关闭windows功能” 打开Windows传真和扫描。

点击“打印服务选项”进入下图:

设置完成可设置为默认打印机


测试效果如下:

8上架(实战)
8.1 设计
1)创建上架任务
什么时候创建上架任务?
上架任务与什么对应?

2)上架流程
上架需要填写哪些信息?
上架的数据库操作有什么?
上架如何存储库存?

8.2 功能开发
1)创建上架任务
阅读需求文档,参考收货功能进行设计开发。
注意:
- 什么时候创建上架任务
- 创建上架任务需要向任务表插入几条记录?
2)查询待上架任务
阅读需求文档,参考查询待收货任务功能设计开发。
测试流程:
- 创建入库单
- 审核入库单
- 创建收货任务
- 收货
- 保存收货完成,此时会自动创建上架任务
- 查询待上架任务,是否正确显示待上架任务
3)上架
首先开发上架接口,然后编写库存操作类,阅读需求文档,参考收货功能设计开发。
测试流程:
- 创建入库单
- 审核入库单
- 创建收货任务
- 收货
- 上架
- 查询库存,观察是否上架成功
4)上架记录查询
阅读需求文档,参考收货记录查询功能设计开发。
测试流程:
- 创建入库单
- 审核入库单
- 创建收货任务
- 收货
- 上架,一个上架任务分一次或多次上架
- 查询上架记录是否正确显示
5)上架接口
拷贝 课程资料-->源代码-->后端--上架--初始代码 到 org.jeecg.modules.wms.inorder.controller下
