Merge branch 'refs/heads/dev-ty1.2' into dev

# Conflicts:
#	mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/BmsServiceFeign.java
#	mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/OmsServiceFeign.java
#	mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/WlhyServiceFeign.java
#	mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/WmsServiceFeign.java
#	mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/YmsServiceFeign.java
#	mhd-common/mhd-common-core/src/main/java/com/mhd/common/core/feign/ThirdPartyServiceFeign.java
#	mhd_oms/src/main/java/com/mhd/oms/infrastructure/feign/ThirdPartyServiceFeign.java
#	mhd_oms/src/main/resources/bootstrap.yml
This commit is contained in:
rcx
2026-09-01 23:49:08 +08:00
111 changed files with 4773 additions and 320 deletions
@@ -0,0 +1,26 @@
package com.mhd.oms.domain.inboundRank.repository.facade;
import com.mhd.oms.domain.inboundRank.repository.po.ExecutionInboundMaterialRankPO;
import com.mhd.oms.domain.inboundRank.repository.po.ExecutionInboundRankTotalPO;
import com.mhd.oms.domain.inboundRank.repository.todo.ExecutionInboundRankDO;
import java.util.List;
/**
* 入库排名统计Service接口
*
* @author rcx
* @date 2026-08-31
*/
public interface IExecutionInboundRankService {
/**
* 商品入库排名列表(仓单+货品汇总,按件数降序)
*/
List<ExecutionInboundMaterialRankPO> queryMaterialRankList(ExecutionInboundRankDO executionInboundRankDO);
/**
* 商品入库排名合计(不分页)
*/
ExecutionInboundRankTotalPO queryMaterialRankTotal(ExecutionInboundRankDO executionInboundRankDO);
}
@@ -0,0 +1,26 @@
package com.mhd.oms.domain.inboundRank.repository.mapper;
import com.mhd.oms.domain.inboundRank.repository.po.ExecutionInboundMaterialRankPO;
import com.mhd.oms.domain.inboundRank.repository.po.ExecutionInboundRankTotalPO;
import com.mhd.oms.domain.inboundRank.repository.todo.ExecutionInboundRankDO;
import java.util.List;
/**
* 入库排名统计Mapper接口
*
* @author rcx
* @date 2026-08-31
*/
public interface ExecutionInboundRankMapper {
/**
* 商品入库排名列表(仓单+货品汇总,按件数降序)
*/
List<ExecutionInboundMaterialRankPO> queryMaterialRankList(ExecutionInboundRankDO executionInboundRankDO);
/**
* 商品入库排名合计(不分页,当前筛选条件全量合计)
*/
ExecutionInboundRankTotalPO queryMaterialRankTotal(ExecutionInboundRankDO executionInboundRankDO);
}
@@ -0,0 +1,34 @@
package com.mhd.oms.domain.inboundRank.repository.persistence;
import com.mhd.oms.domain.inboundRank.repository.facade.IExecutionInboundRankService;
import com.mhd.oms.domain.inboundRank.repository.mapper.ExecutionInboundRankMapper;
import com.mhd.oms.domain.inboundRank.repository.po.ExecutionInboundMaterialRankPO;
import com.mhd.oms.domain.inboundRank.repository.po.ExecutionInboundRankTotalPO;
import com.mhd.oms.domain.inboundRank.repository.todo.ExecutionInboundRankDO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 入库排名统计Service业务层处理
*
* @author rcx
* @date 2026-08-31
*/
@Service
public class ExecutionInboundRankImpl implements IExecutionInboundRankService {
@Autowired
private ExecutionInboundRankMapper executionInboundRankMapper;
@Override
public List<ExecutionInboundMaterialRankPO> queryMaterialRankList(ExecutionInboundRankDO executionInboundRankDO) {
return executionInboundRankMapper.queryMaterialRankList(executionInboundRankDO);
}
@Override
public ExecutionInboundRankTotalPO queryMaterialRankTotal(ExecutionInboundRankDO executionInboundRankDO) {
return executionInboundRankMapper.queryMaterialRankTotal(executionInboundRankDO);
}
}
@@ -0,0 +1,81 @@
package com.mhd.oms.domain.inboundRank.repository.po;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.mhd.common.core.annotation.Excel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 入库排名统计PO(一行 = 某仓单内某货品的合计,按件数降序排名)
*
* <p>数据源跨库取 WMS 入库表(NGWL_TEST_WMS.stock_in_order / in_material_detailOMS 执行单状态/上架量不回传),
* 口径与 WMS 端 /inboundRankApi 完全一致,两边数字一致。
* 字段口径:入仓日期=WMS入仓单入仓日期(为空兜底建档时间);仓单编号=WMS入库单号;客户编号/存货人=入仓单货主;
* 件数/毛重/体积=仓单内该货品明细合计(件数取实际上架量 SHELVES_QUANTITY);毛重(吨)=明细总毛重KG/1000
* 投保总额=货品档案保额(NGWL_TEST_WMS.material_base_info.insure_amount,组内取MAX防多批次明细行重复累加)</p>
*
* @author rcx
* @date 2026-08-31
*/
@Data
@ApiModel(value = "入库排名统计对象", description = "入库排名统计响应对象")
public class ExecutionInboundMaterialRankPO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("入仓日期")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd")
@Excel(name = "入仓日期", width = 30, dateFormat = "yyyy-MM-dd")
private Date warehouseDate;
@ApiModelProperty("仓单编号(WMS入库单号)")
@Excel(name = "仓单编号")
private String inOrderNumber;
@ApiModelProperty("客户编号")
@Excel(name = "客户编号")
private String shipperCode;
@ApiModelProperty("存货人(客户名称)")
@Excel(name = "存货人")
private String shipperName;
@ApiModelProperty("商品货号")
@Excel(name = "商品货号")
private String materialCode;
@ApiModelProperty("货名/商品名称")
@Excel(name = "货名")
private String materialName;
@ApiModelProperty("规格型号")
@Excel(name = "规格型号")
private String specificationModel;
@ApiModelProperty("单位")
@Excel(name = "单位")
private String unitName;
@ApiModelProperty("件数(仓单内该货品实际上架量合计 SHELVES_QUANTITY,排名依据)")
@Excel(name = "件数")
private BigDecimal pieceCount;
@ApiModelProperty("毛重(吨)(仓单内该货品合计,KG/1000)")
@Excel(name = "毛重(吨)")
private BigDecimal grossWeightTon;
@ApiModelProperty("投保总额(货品档案保额)")
@Excel(name = "投保总额")
private BigDecimal insureAmount;
@ApiModelProperty("体积(CBM)(仓单内该货品合计)")
@Excel(name = "体积(CBM)")
private BigDecimal totalVolume;
}
@@ -0,0 +1,39 @@
package com.mhd.oms.domain.inboundRank.repository.po;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 入库排名统计合计PO
*
* <p>当前筛选条件下不分页的全量合计,随分页列表一起返回
* totalPieceCount/totalGrossWeightTon/totalInsureAmount/totalVolume/totalOrderCount</p>
*
* @author rcx
* @date 2026-08-31
*/
@Data
@ApiModel(value = "入库排名统计合计对象", description = "入库排名统计合计响应对象")
public class ExecutionInboundRankTotalPO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("件数合计(实际上架量)")
private BigDecimal totalPieceCount;
@ApiModelProperty("毛重(吨)合计")
private BigDecimal totalGrossWeightTon;
@ApiModelProperty("投保总额合计(各仓单+货品行保额之和)")
private BigDecimal totalInsureAmount;
@ApiModelProperty("体积(CBM)合计")
private BigDecimal totalVolume;
@ApiModelProperty("仓单数合计")
private Long totalOrderCount;
}
@@ -0,0 +1,47 @@
package com.mhd.oms.domain.inboundRank.repository.todo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 入库排名统计查询DO
*
* <p>topOrganizationId 由服务端按登录用户注入,前端不传</p>
*
* @author rcx
* @date 2026-08-31
*/
@Data
public class ExecutionInboundRankDO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("入仓日期-开始 yyyy-MM-dd(必填)")
private String warehouseDateStart;
@ApiModelProperty("入仓日期-结束 yyyy-MM-dd(必填)")
private String warehouseDateEnd;
@ApiModelProperty("仓单编号/入库单号(模糊)")
private String inOrderNumber;
@ApiModelProperty("客户编号(模糊)")
private String shipperCode;
@ApiModelProperty("存货人/客户名称(模糊)")
private String shipperName;
@ApiModelProperty("商品货号(模糊)")
private String materialCode;
@ApiModelProperty("货名/商品名称(模糊)")
private String materialName;
@ApiModelProperty("仓库名称(模糊)")
private String warehouseName;
@ApiModelProperty("一级组织表ID(登录用户隔离,非1时注入)")
private Long topOrganizationId;
}
@@ -0,0 +1,44 @@
package com.mhd.oms.domain.inboundRank.repository.todo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 入库排名统计入参DTO
*
* <p>数据源跨库取 WMS 入库链路(NGWL_TEST_WMS.stock_in_order / in_material_detailOMS 执行单状态/上架量不回传)</p>
*
* @author rcx
* @date 2026-08-31
*/
@Data
public class ExecutionInboundRankDTO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("入仓日期-开始 yyyy-MM-dd(必填,按入仓单入仓日期筛选)")
private String warehouseDateStart;
@ApiModelProperty("入仓日期-结束 yyyy-MM-dd(必填,按入仓单入仓日期筛选)")
private String warehouseDateEnd;
@ApiModelProperty("仓单编号/入库单号(模糊)")
private String inOrderNumber;
@ApiModelProperty("客户编号(模糊)")
private String shipperCode;
@ApiModelProperty("存货人/客户名称(模糊)")
private String shipperName;
@ApiModelProperty("商品货号(模糊)")
private String materialCode;
@ApiModelProperty("货名/商品名称(模糊)")
private String materialName;
@ApiModelProperty("仓库名称(模糊)")
private String warehouseName;
}
@@ -0,0 +1,69 @@
package com.mhd.oms.domain.inboundRank.service;
import com.mhd.common.core.exception.ServiceException;
import com.mhd.common.core.utils.StringUtils;
import com.mhd.common.security.utils.SecurityUtils;
import com.mhd.system.api.model.LoginUser;
import com.mhd.oms.domain.inboundRank.repository.po.ExecutionInboundMaterialRankPO;
import com.mhd.oms.domain.inboundRank.repository.po.ExecutionInboundRankTotalPO;
import com.mhd.oms.domain.inboundRank.repository.todo.ExecutionInboundRankDO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 入库排名统计应用服务
*
* <p>负责登录用户组织隔离注入、日期区间校验,查询逻辑在 ExecutionInboundRankDomainService → ExecutionInboundRankMapper.xml</p>
*
* @author rcx
* @date 2026-08-31
*/
@Service
@Slf4j
public class ExecutionInboundRankApplicationService {
@Autowired
private ExecutionInboundRankDomainService executionInboundRankDomainService;
/**
* 分页查询商品入库排名列表(仓单+货品汇总,按件数降序)
*/
public List<ExecutionInboundMaterialRankPO> queryMaterialRankList(ExecutionInboundRankDO executionInboundRankDO) {
fillLoginContext(executionInboundRankDO);
return executionInboundRankDomainService.queryMaterialRankList(executionInboundRankDO);
}
/**
* 商品入库排名合计(当前筛选条件全量,不分页)
*/
public ExecutionInboundRankTotalPO queryMaterialRankTotal(ExecutionInboundRankDO executionInboundRankDO) {
return executionInboundRankDomainService.queryMaterialRankTotal(executionInboundRankDO);
}
/**
* 导出商品入库排名(不分页全量)
*/
public List<ExecutionInboundMaterialRankPO> exportMaterialRank(ExecutionInboundRankDO executionInboundRankDO) {
fillLoginContext(executionInboundRankDO);
return executionInboundRankDomainService.queryMaterialRankList(executionInboundRankDO);
}
/**
* 注入登录用户组织隔离,并校验入仓日期区间必填
*/
private void fillLoginContext(ExecutionInboundRankDO executionInboundRankDO) {
if (StringUtils.isEmpty(executionInboundRankDO.getWarehouseDateStart()) || StringUtils.isEmpty(executionInboundRankDO.getWarehouseDateEnd())) {
throw new ServiceException("请选择统计日期区间");
}
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null && loginUser.getUserPo() != null) {
Long topOrganizationId = loginUser.getUserPo().getTopOrganizationId();
if (topOrganizationId != null && topOrganizationId != 1) {
executionInboundRankDO.setTopOrganizationId(topOrganizationId);
}
}
}
}
@@ -0,0 +1,36 @@
package com.mhd.oms.domain.inboundRank.service;
import cn.hutool.core.util.ObjectUtil;
import com.mhd.common.core.exception.digitalLogisticsException.DigitalLogisticsException;
import com.mhd.common.core.exception.digitalLogisticsException.UserError;
import com.mhd.common.core.utils.IgnoreNullUtil;
import com.mhd.common.core.utils.bean.BeanUtils;
import com.mhd.common.security.utils.SecurityUtils;
import com.mhd.system.api.model.LoginUser;
import com.mhd.oms.domain.inboundRank.repository.todo.ExecutionInboundRankDO;
import com.mhd.oms.domain.inboundRank.repository.todo.ExecutionInboundRankDTO;
import org.springframework.stereotype.Component;
/**
* 入库排名统计Assembler
*
* @author rcx
* @date 2026-08-31
*/
@Component
public class ExecutionInboundRankAssembler {
/**
* 转换实体
*/
public ExecutionInboundRankDO toDO(ExecutionInboundRankDTO executionInboundRankDTO) {
ExecutionInboundRankDO executionInboundRankDO = new ExecutionInboundRankDO();
// 拷贝
BeanUtils.copyProperties(executionInboundRankDTO, executionInboundRankDO, IgnoreNullUtil.getNullPropertyNames(executionInboundRankDTO));
LoginUser loginUser = SecurityUtils.getLoginUser();
if (ObjectUtil.isNull(loginUser)) {
throw new DigitalLogisticsException(UserError.TIMEOUT);
}
return executionInboundRankDO;
}
}
@@ -0,0 +1,39 @@
package com.mhd.oms.domain.inboundRank.service;
import com.mhd.oms.domain.inboundRank.repository.facade.IExecutionInboundRankService;
import com.mhd.oms.domain.inboundRank.repository.po.ExecutionInboundMaterialRankPO;
import com.mhd.oms.domain.inboundRank.repository.po.ExecutionInboundRankTotalPO;
import com.mhd.oms.domain.inboundRank.repository.todo.ExecutionInboundRankDO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 入库排名统计领域服务
*
* @author rcx
* @date 2026-08-31
*/
@Service
@Slf4j
public class ExecutionInboundRankDomainService {
@Autowired
private IExecutionInboundRankService executionInboundRankService;
/**
* 商品入库排名列表(仓单+货品汇总,按件数降序)
*/
public List<ExecutionInboundMaterialRankPO> queryMaterialRankList(ExecutionInboundRankDO executionInboundRankDO) {
return executionInboundRankService.queryMaterialRankList(executionInboundRankDO);
}
/**
* 商品入库排名合计(不分页)
*/
public ExecutionInboundRankTotalPO queryMaterialRankTotal(ExecutionInboundRankDO executionInboundRankDO) {
return executionInboundRankService.queryMaterialRankTotal(executionInboundRankDO);
}
}
@@ -0,0 +1,37 @@
package com.mhd.oms.domain.outboundRank.repository.facade;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundLineRankPO;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundMaterialRankPO;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundRankTotalPO;
import com.mhd.oms.domain.outboundRank.repository.todo.ExecutionOutboundRankDO;
import java.util.List;
/**
* 出库排名统计Service接口
*
* @author rcx
* @date 2026-08-31
*/
public interface IExecutionOutboundRankService {
/**
* 商品出库排名列表
*/
List<ExecutionOutboundMaterialRankPO> queryMaterialRankList(ExecutionOutboundRankDO executionOutboundRankDO);
/**
* 线路出库排名列表
*/
List<ExecutionOutboundLineRankPO> queryLineRankList(ExecutionOutboundRankDO executionOutboundRankDO);
/**
* 商品出库排名合计(不分页)
*/
ExecutionOutboundRankTotalPO queryMaterialRankTotal(ExecutionOutboundRankDO executionOutboundRankDO);
/**
* 线路出库排名合计(不分页)
*/
ExecutionOutboundRankTotalPO queryLineRankTotal(ExecutionOutboundRankDO executionOutboundRankDO);
}
@@ -0,0 +1,37 @@
package com.mhd.oms.domain.outboundRank.repository.mapper;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundLineRankPO;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundMaterialRankPO;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundRankTotalPO;
import com.mhd.oms.domain.outboundRank.repository.todo.ExecutionOutboundRankDO;
import java.util.List;
/**
* 出库排名统计Mapper接口
*
* @author rcx
* @date 2026-08-31
*/
public interface ExecutionOutboundRankMapper {
/**
* 商品出库排名列表(按销售量倒序)
*/
List<ExecutionOutboundMaterialRankPO> queryMaterialRankList(ExecutionOutboundRankDO executionOutboundRankDO);
/**
* 线路出库排名列表(按销售量倒序)
*/
List<ExecutionOutboundLineRankPO> queryLineRankList(ExecutionOutboundRankDO executionOutboundRankDO);
/**
* 商品出库排名合计(不分页,当前筛选条件全量合计)
*/
ExecutionOutboundRankTotalPO queryMaterialRankTotal(ExecutionOutboundRankDO executionOutboundRankDO);
/**
* 线路出库排名合计(不分页,当前筛选条件全量合计)
*/
ExecutionOutboundRankTotalPO queryLineRankTotal(ExecutionOutboundRankDO executionOutboundRankDO);
}
@@ -0,0 +1,45 @@
package com.mhd.oms.domain.outboundRank.repository.persistence;
import com.mhd.oms.domain.outboundRank.repository.facade.IExecutionOutboundRankService;
import com.mhd.oms.domain.outboundRank.repository.mapper.ExecutionOutboundRankMapper;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundLineRankPO;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundMaterialRankPO;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundRankTotalPO;
import com.mhd.oms.domain.outboundRank.repository.todo.ExecutionOutboundRankDO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 出库排名统计Service业务层处理
*
* @author rcx
* @date 2026-08-31
*/
@Service
public class ExecutionOutboundRankImpl implements IExecutionOutboundRankService {
@Autowired
private ExecutionOutboundRankMapper executionOutboundRankMapper;
@Override
public List<ExecutionOutboundMaterialRankPO> queryMaterialRankList(ExecutionOutboundRankDO executionOutboundRankDO) {
return executionOutboundRankMapper.queryMaterialRankList(executionOutboundRankDO);
}
@Override
public List<ExecutionOutboundLineRankPO> queryLineRankList(ExecutionOutboundRankDO executionOutboundRankDO) {
return executionOutboundRankMapper.queryLineRankList(executionOutboundRankDO);
}
@Override
public ExecutionOutboundRankTotalPO queryMaterialRankTotal(ExecutionOutboundRankDO executionOutboundRankDO) {
return executionOutboundRankMapper.queryMaterialRankTotal(executionOutboundRankDO);
}
@Override
public ExecutionOutboundRankTotalPO queryLineRankTotal(ExecutionOutboundRankDO executionOutboundRankDO) {
return executionOutboundRankMapper.queryLineRankTotal(executionOutboundRankDO);
}
}
@@ -0,0 +1,45 @@
package com.mhd.oms.domain.outboundRank.repository.po;
import com.mhd.common.core.annotation.Excel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 线路出库排名PO
*
* <p>按日期区间对出库单聚合:销售量=SUM(出库数量),交易数=COUNT(DISTINCT 出库单号),金额=SUM(明细总价);
* 仅统计带线路的出库单(line_code 为 2026-08-20 新增冗余字段,历史单为空)</p>
*
* @author rcx
* @date 2026-08-31
*/
@Data
@ApiModel(value = "线路出库排名对象", description = "线路出库排名响应对象")
public class ExecutionOutboundLineRankPO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("线路编码")
@Excel(name = "线路编码")
private String lineCode;
@ApiModelProperty("线路名称")
@Excel(name = "线路名称")
private String lineName;
@ApiModelProperty("销售量(出库数量合计)")
@Excel(name = "销售量")
private BigDecimal salesQuantity;
@ApiModelProperty("交易数(出库单据数)")
@Excel(name = "交易数")
private Long tradeCount;
@ApiModelProperty("金额")
@Excel(name = "金额")
private BigDecimal amount;
}
@@ -0,0 +1,52 @@
package com.mhd.oms.domain.outboundRank.repository.po;
import com.mhd.common.core.annotation.Excel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 商品出库排名PO
*
* <p>按日期区间对出库明细聚合:销售量=SUM(出库数量),交易数=COUNT(DISTINCT 出库单号),金额=SUM(明细总价)</p>
*
* @author rcx
* @date 2026-08-31
*/
@Data
@ApiModel(value = "商品出库排名对象", description = "商品出库排名响应对象")
public class ExecutionOutboundMaterialRankPO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("商品货号")
@Excel(name = "商品货号")
private String materialCode;
@ApiModelProperty("商品名称")
@Excel(name = "商品名称")
private String materialName;
@ApiModelProperty("规格")
@Excel(name = "规格")
private String specificationModel;
@ApiModelProperty("单位")
@Excel(name = "单位")
private String unitName;
@ApiModelProperty("销售量(出库数量合计)")
@Excel(name = "销售量")
private BigDecimal salesQuantity;
@ApiModelProperty("交易数(出库单据数)")
@Excel(name = "交易数")
private Long tradeCount;
@ApiModelProperty("金额")
@Excel(name = "金额")
private BigDecimal amount;
}
@@ -0,0 +1,32 @@
package com.mhd.oms.domain.outboundRank.repository.po;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 出库排名合计PO
*
* <p>当前筛选条件下不分页的全量合计,随分页列表一起返回(totalSalesQuantity/totalTradeCount/totalAmount</p>
*
* @author rcx
* @date 2026-08-31
*/
@Data
@ApiModel(value = "出库排名合计对象", description = "出库排名合计响应对象")
public class ExecutionOutboundRankTotalPO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("销售量合计")
private BigDecimal totalSalesQuantity;
@ApiModelProperty("交易数合计(去重出库单数)")
private Long totalTradeCount;
@ApiModelProperty("金额合计")
private BigDecimal totalAmount;
}
@@ -0,0 +1,50 @@
package com.mhd.oms.domain.outboundRank.repository.todo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 出库排名统计查询DO
*
* <p>topOrganizationId 由服务端按登录用户注入,前端不传</p>
*
* @author rcx
* @date 2026-08-31
*/
@Data
public class ExecutionOutboundRankDO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("出库日期-开始 yyyy-MM-dd(必填)")
private String createTimeStart;
@ApiModelProperty("出库日期-结束 yyyy-MM-dd(必填)")
private String createTimeEnd;
@ApiModelProperty("商品货号(模糊)")
private String materialCode;
@ApiModelProperty("商品名称(模糊)")
private String materialName;
@ApiModelProperty("线路编码(模糊)")
private String lineCode;
@ApiModelProperty("线路名称(模糊)")
private String lineName;
@ApiModelProperty("仓库名称(模糊)")
private String warehouseName;
@ApiModelProperty("客户名称(模糊)")
private String customerName;
@ApiModelProperty("货主名称(模糊)")
private String shipperName;
@ApiModelProperty("一级组织表ID(登录用户隔离,非1时注入)")
private Long topOrganizationId;
}
@@ -0,0 +1,46 @@
package com.mhd.oms.domain.outboundRank.repository.todo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 出库排名统计入参DTO
*
* <p>按日期区间统计商品/线路出库量排名。OMS 库出库单状态不流转(无"已出库"数据),
* 数据源跨库取 WMS 库出库表(NGWL_TEST_WMS.stock_out_order / out_material_detail),与 WMS 端 /outboundRankApi 同口径</p>
*
* @author rcx
* @date 2026-08-31
*/
@Data
public class ExecutionOutboundRankDTO {
@ApiModelProperty("出库日期-开始 yyyy-MM-dd(必填,按出库单创建时间统计)")
private String createTimeStart;
@ApiModelProperty("出库日期-结束 yyyy-MM-dd(必填,按出库单创建时间统计)")
private String createTimeEnd;
@ApiModelProperty("商品货号(模糊)")
private String materialCode;
@ApiModelProperty("商品名称(模糊)")
private String materialName;
@ApiModelProperty("线路编码(模糊)")
private String lineCode;
@ApiModelProperty("线路名称(模糊)")
private String lineName;
@ApiModelProperty("仓库名称(模糊)")
private String warehouseName;
@ApiModelProperty("客户名称(模糊)")
private String customerName;
@ApiModelProperty("货主名称(模糊)")
private String shipperName;
}
@@ -0,0 +1,93 @@
package com.mhd.oms.domain.outboundRank.service;
import com.mhd.common.core.exception.ServiceException;
import com.mhd.common.core.utils.StringUtils;
import com.mhd.common.security.utils.SecurityUtils;
import com.mhd.system.api.model.LoginUser;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundLineRankPO;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundMaterialRankPO;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundRankTotalPO;
import com.mhd.oms.domain.outboundRank.repository.todo.ExecutionOutboundRankDO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 出库排名统计应用服务
*
* <p>负责登录用户组织隔离注入、日期区间校验,统计逻辑在 ExecutionOutboundRankDomainService → ExecutionOutboundRankMapper.xml(跨库取 WMS 出库数据)</p>
*
* @author rcx
* @date 2026-08-31
*/
@Service
@Slf4j
public class ExecutionOutboundRankApplicationService {
@Autowired
private ExecutionOutboundRankDomainService executionOutboundRankDomainService;
/**
* 分页查询商品出库排名列表
*/
public List<ExecutionOutboundMaterialRankPO> queryMaterialRankList(ExecutionOutboundRankDO executionOutboundRankDO) {
fillLoginContext(executionOutboundRankDO);
return executionOutboundRankDomainService.queryMaterialRankList(executionOutboundRankDO);
}
/**
* 分页查询线路出库排名列表
*/
public List<ExecutionOutboundLineRankPO> queryLineRankList(ExecutionOutboundRankDO executionOutboundRankDO) {
fillLoginContext(executionOutboundRankDO);
return executionOutboundRankDomainService.queryLineRankList(executionOutboundRankDO);
}
/**
* 商品出库排名合计(当前筛选条件全量,不分页)
*/
public ExecutionOutboundRankTotalPO queryMaterialRankTotal(ExecutionOutboundRankDO executionOutboundRankDO) {
return executionOutboundRankDomainService.queryMaterialRankTotal(executionOutboundRankDO);
}
/**
* 线路出库排名合计(当前筛选条件全量,不分页)
*/
public ExecutionOutboundRankTotalPO queryLineRankTotal(ExecutionOutboundRankDO executionOutboundRankDO) {
return executionOutboundRankDomainService.queryLineRankTotal(executionOutboundRankDO);
}
/**
* 导出商品出库排名(不分页全量)
*/
public List<ExecutionOutboundMaterialRankPO> exportMaterialRankList(ExecutionOutboundRankDO executionOutboundRankDO) {
fillLoginContext(executionOutboundRankDO);
return executionOutboundRankDomainService.queryMaterialRankList(executionOutboundRankDO);
}
/**
* 导出线路出库排名(不分页全量)
*/
public List<ExecutionOutboundLineRankPO> exportLineRankList(ExecutionOutboundRankDO executionOutboundRankDO) {
fillLoginContext(executionOutboundRankDO);
return executionOutboundRankDomainService.queryLineRankList(executionOutboundRankDO);
}
/**
* 注入登录用户组织隔离,并校验日期区间必填
*/
private void fillLoginContext(ExecutionOutboundRankDO executionOutboundRankDO) {
if (StringUtils.isEmpty(executionOutboundRankDO.getCreateTimeStart()) || StringUtils.isEmpty(executionOutboundRankDO.getCreateTimeEnd())) {
throw new ServiceException("请选择统计日期区间");
}
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null && loginUser.getUserPo() != null) {
Long topOrganizationId = loginUser.getUserPo().getTopOrganizationId();
if (topOrganizationId != null && topOrganizationId != 1) {
executionOutboundRankDO.setTopOrganizationId(topOrganizationId);
}
}
}
}
@@ -0,0 +1,36 @@
package com.mhd.oms.domain.outboundRank.service;
import cn.hutool.core.util.ObjectUtil;
import com.mhd.common.core.exception.digitalLogisticsException.DigitalLogisticsException;
import com.mhd.common.core.exception.digitalLogisticsException.UserError;
import com.mhd.common.core.utils.IgnoreNullUtil;
import com.mhd.common.core.utils.bean.BeanUtils;
import com.mhd.common.security.utils.SecurityUtils;
import com.mhd.system.api.model.LoginUser;
import com.mhd.oms.domain.outboundRank.repository.todo.ExecutionOutboundRankDO;
import com.mhd.oms.domain.outboundRank.repository.todo.ExecutionOutboundRankDTO;
import org.springframework.stereotype.Component;
/**
* 出库排名统计Assembler
*
* @author rcx
* @date 2026-08-31
*/
@Component
public class ExecutionOutboundRankAssembler {
/**
* 转换实体
*/
public ExecutionOutboundRankDO toDO(ExecutionOutboundRankDTO executionOutboundRankDTO) {
ExecutionOutboundRankDO executionOutboundRankDO = new ExecutionOutboundRankDO();
// 拷贝
BeanUtils.copyProperties(executionOutboundRankDTO, executionOutboundRankDO, IgnoreNullUtil.getNullPropertyNames(executionOutboundRankDTO));
LoginUser loginUser = SecurityUtils.getLoginUser();
if (ObjectUtil.isNull(loginUser)) {
throw new DigitalLogisticsException(UserError.TIMEOUT);
}
return executionOutboundRankDO;
}
}
@@ -0,0 +1,54 @@
package com.mhd.oms.domain.outboundRank.service;
import com.mhd.oms.domain.outboundRank.repository.facade.IExecutionOutboundRankService;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundLineRankPO;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundMaterialRankPO;
import com.mhd.oms.domain.outboundRank.repository.po.ExecutionOutboundRankTotalPO;
import com.mhd.oms.domain.outboundRank.repository.todo.ExecutionOutboundRankDO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 出库排名统计领域服务
*
* @author rcx
* @date 2026-08-31
*/
@Service
@Slf4j
public class ExecutionOutboundRankDomainService {
@Autowired
private IExecutionOutboundRankService executionOutboundRankService;
/**
* 商品出库排名列表
*/
public List<ExecutionOutboundMaterialRankPO> queryMaterialRankList(ExecutionOutboundRankDO executionOutboundRankDO) {
return executionOutboundRankService.queryMaterialRankList(executionOutboundRankDO);
}
/**
* 线路出库排名列表
*/
public List<ExecutionOutboundLineRankPO> queryLineRankList(ExecutionOutboundRankDO executionOutboundRankDO) {
return executionOutboundRankService.queryLineRankList(executionOutboundRankDO);
}
/**
* 商品出库排名合计(不分页)
*/
public ExecutionOutboundRankTotalPO queryMaterialRankTotal(ExecutionOutboundRankDO executionOutboundRankDO) {
return executionOutboundRankService.queryMaterialRankTotal(executionOutboundRankDO);
}
/**
* 线路出库排名合计(不分页)
*/
public ExecutionOutboundRankTotalPO queryLineRankTotal(ExecutionOutboundRankDO executionOutboundRankDO) {
return executionOutboundRankService.queryLineRankTotal(executionOutboundRankDO);
}
}
@@ -210,6 +210,12 @@ public class ReservationMaterialInventory extends BaseVOEntity {
@Excel(name = "存货日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date inventoryDate;
@ApiModelProperty("进货日期")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "进货日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date purchaseDate;
@ApiModelProperty("ExtAttr3")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@@ -2,6 +2,7 @@ package com.mhd.oms.domain.reservationMaterialInventory.repository.facade;
import com.baomidou.mybatisplus.extension.service.IService;
import com.mhd.oms.domain.reservationMaterialInventory.entity.ReservationMaterialInventory;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.MaterialInventoryExpiryBatchPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.ReservationMaterialInventoryPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.ReservationMaterialInventoryQueryListPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.todo.MaterialInventoryParamDO;
@@ -113,4 +114,9 @@ public interface IReservationMaterialInventoryService extends IService<Reservati
public void xgkc(MaterialInventoryOmsParamDO materialInventoryParamDO);
public void kctzjl(MaterialInventoryOmsParamDO materialInventoryParamDO,String adjustAction,LoginUser loginUser);
/**
* 根据物料编号查询库存批次最近5条过期日期+批次号(入库业务单新增页面下拉回显)
*/
public List<MaterialInventoryExpiryBatchPO> selectRecentExpiryBatches(String materialCode, Long topOrganizationId);
}
@@ -2,6 +2,7 @@ package com.mhd.oms.domain.reservationMaterialInventory.repository.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mhd.oms.domain.reservationMaterialInventory.entity.ReservationMaterialInventory;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.MaterialInventoryExpiryBatchPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.ReservationMaterialInventoryPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.todo.MaterialBarCodePO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.todo.MaterialBaseInfo;
@@ -36,4 +37,7 @@ public interface ReservationMaterialInventoryMapper extends BaseMapper<Reservati
public MaterialBaseInfo getByInfoId(@Param("id") Long id);
public List<MaterialBarCodePO> getInfoByMaterialBaseInfoIds(@Param("id") Long id);
//入库业务单新增:根据物料编号查询库存批次去重后的过期日期+批次号(最近5条),供前端下拉回显
public List<MaterialInventoryExpiryBatchPO> selectRecentExpiryBatches(@Param("materialCode") String materialCode, @Param("topOrganizationId") Long topOrganizationId);
}
@@ -13,6 +13,7 @@ import com.mhd.oms.domain.reservationInventoryAdjustmentRecord.repository.mapper
import com.mhd.oms.domain.reservationMaterialInventory.entity.ReservationMaterialInventory;
import com.mhd.oms.domain.reservationMaterialInventory.repository.facade.IReservationMaterialInventoryService;
import com.mhd.oms.domain.reservationMaterialInventory.repository.mapper.ReservationMaterialInventoryMapper;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.MaterialInventoryExpiryBatchPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.ReservationMaterialInventoryPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.ReservationMaterialInventoryQueryListPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.todo.MaterialBarCodePO;
@@ -120,6 +121,14 @@ public class ReservationMaterialInventoryImpl extends ServiceImpl<ReservationMat
return materialInventoryPO;
}
/**
* 根据物料编号查询库存批次最近5条过期日期+批次号(入库业务单新增页面下拉回显)
*/
@Override
public List<MaterialInventoryExpiryBatchPO> selectRecentExpiryBatches(String materialCode, Long topOrganizationId) {
return materialInventoryMapper.selectRecentExpiryBatches(materialCode, topOrganizationId);
}
@Override
public Boolean omsEdit(MaterialInventoryOmsParamDO materialInventoryOmsParamDO) {
String inOrderNumber = materialInventoryOmsParamDO.getInOrderNumber();
@@ -127,6 +136,9 @@ public class ReservationMaterialInventoryImpl extends ServiceImpl<ReservationMat
String batchNumber = materialInventoryOmsParamDO.getBatchNumber();
Long materialBaseInfoId = materialInventoryOmsParamDO.getMaterialBaseInfoId();
Long warehouseId = materialInventoryOmsParamDO.getWarehouseId();
// 进货日期参与行匹配:WMS 库存行按批次属性(含进货日期)分行后,OMS 同步按进货日期区分,避免不同进货日期误合并;
// 为空时匹配 purchase_date 为空的行,存量行为不变
Date purchaseDate = materialInventoryOmsParamDO.getPurchaseDate();
LambdaQueryWrapper<ReservationMaterialInventory> param = new LambdaQueryWrapper<ReservationMaterialInventory>()
// .eq(ReservationMaterialInventory::getInOrderNumber, inOrderNumber)
//.eq(ReservationMaterialInventory::getLotNumber, lotNumber)
@@ -148,6 +160,11 @@ public class ReservationMaterialInventoryImpl extends ServiceImpl<ReservationMat
} else {
param.isNull(ReservationMaterialInventory::getBatchNumber);
}
if (purchaseDate != null) {
param.eq(ReservationMaterialInventory::getPurchaseDate, purchaseDate);
} else {
param.isNull(ReservationMaterialInventory::getPurchaseDate);
}
List<ReservationMaterialInventory> reservationMaterialInventories = materialInventoryMapper.selectList(param);
if (reservationMaterialInventories != null && reservationMaterialInventories.size() > 0) {
ReservationMaterialInventory reservationMaterialInventory = reservationMaterialInventories.get(0);
@@ -542,6 +559,7 @@ public class ReservationMaterialInventoryImpl extends ServiceImpl<ReservationMat
// materialInventory.setExtAttr4(materialInventoryParamDO.getExtAttr4());
materialInventory.setExpiryDate(materialInventoryParamDO.getExpiryDate());
// materialInventory.setInventoryDate(materialInventoryParamDO.getInventoryDate());
materialInventory.setPurchaseDate(materialInventoryParamDO.getPurchaseDate());
materialInventory.setBatchNumber(materialInventoryParamDO.getBatchNumber());
String batchNo = materialInventoryParamDO.getBatchNo();
if (batchNo != null) {
@@ -0,0 +1,25 @@
package com.mhd.oms.domain.reservationMaterialInventory.repository.po;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 物料库存批次过期日期对象(入库业务单新增页面下拉回显)
*
* @author rcx
* @date 2026-08-31
*/
@Data
@ApiModel(value = "物料库存批次过期日期", description = "物料库存批次过期日期响应对象")
public class MaterialInventoryExpiryBatchPO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("过期日期 yyyy-MM-dd")
private String expiryDate;
@ApiModelProperty("批次号")
private String batchNumber;
}
@@ -192,6 +192,12 @@ public class ReservationMaterialInventoryPO extends MaterialBasePO {
@Excel(name = "存货日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date inventoryDate;
@ApiModelProperty("进货日期")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "进货日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date purchaseDate;
@ApiModelProperty("ExtAttr3")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@@ -217,6 +217,12 @@ public class ReservationMaterialInventoryDO extends MaterialBaseDO {
@Excel(name = "存货日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date inventoryDate;
@ApiModelProperty("进货日期")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "进货日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date purchaseDate;
@ApiModelProperty("ExtAttr3")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@@ -253,9 +259,18 @@ public class ReservationMaterialInventoryDO extends MaterialBaseDO {
private String createTimeEnd;
private String expiryDateStr;
private String productionDateStr;
private String purchaseDateStr;
private String expiryDateStart;
private String expiryDateEnd;
private String purchaseDateStart;
private String purchaseDateEnd;
@ApiModelProperty("导出按到期日分组排序标识:仅 queryRule=wlExpiry 时生效,true-到期日倒序(日期近的在前、空值最后、0库存靠后);不传-保持原有排序")
private Boolean orderByExpiryDate;
@ApiModelProperty("导出排除库存为0标识:true-排除库存数量汇总为0的数据(所有查询维度 queryRule 均生效);不传-不过滤")
private Boolean excludeZeroInventoryQuantity;
@ApiModelProperty("导出标识:导出与查询共用 /list,true-本次为导出请求,启用导出融合排序(到期日倒序优先→同到期日按物料编码升序→0库存靠后→更新时间倒序);不传-保持原有排序")
private Boolean isExport;
}
@@ -193,5 +193,7 @@ public class ReservationMaterialInventoryDTO extends MaterialBaseDTO {
private String createTimeEnd;
private String expiryDate;
private String productionDate;
/** 进货日期(yyyy-MM-dd,单日精确查询) */
private String purchaseDate;
}
@@ -8,6 +8,7 @@ import com.mhd.common.core.utils.bean.BeanUtils;
import com.mhd.common.core.web.domain.AjaxResult;
import com.mhd.common.security.utils.SecurityUtils;
import com.mhd.oms.domain.reservationMaterialInventory.repository.mapper.ReservationMaterialInventoryMapper;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.MaterialInventoryExpiryBatchPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.ReservationMaterialInventoryPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.ReservationMaterialInventoryQueryListPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.todo.MaterialBarCodePO;
@@ -185,6 +186,22 @@ public class ReservationMaterialInventoryApplicationService {
return materialInventoryDomainService.getInventoryByStorageLocationIdAndMaterialBaseInfoId(storageLocationId, materialBaseInfoId);
}
/**
* 根据物料编号查询库存批次最近5条过期日期+批次号(入库业务单新增页面下拉回显)
* 与 queryList 一致:按登录用户一级组织(租户)过滤,一级组织(平台)不限制
*/
public List<MaterialInventoryExpiryBatchPO> selectRecentExpiryBatches(String materialCode) {
Long topOrganizationId = null;
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null && loginUser.getUserPo() != null) {
Long loginTopOrgId = loginUser.getUserPo().getTopOrganizationId();
if (loginTopOrgId != null && loginTopOrgId != 1) {
topOrganizationId = loginTopOrgId;
}
}
return materialInventoryDomainService.selectRecentExpiryBatches(materialCode, topOrganizationId);
}
/**
* @description:库存查询列表
* @param materialInventoryQueryListDO
@@ -3,6 +3,7 @@ package com.mhd.oms.domain.reservationMaterialInventory.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.mhd.oms.domain.reservationMaterialInventory.entity.ReservationMaterialInventory;
import com.mhd.oms.domain.reservationMaterialInventory.repository.facade.IReservationMaterialInventoryService;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.MaterialInventoryExpiryBatchPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.ReservationMaterialInventoryPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.po.ReservationMaterialInventoryQueryListPO;
import com.mhd.oms.domain.reservationMaterialInventory.repository.todo.ReservationMaterialInventoryDO;
@@ -88,6 +89,13 @@ public class ReservationMaterialInventoryDomainService {
return materialInventory.getInventoryQuantity();
}
/**
* 根据物料编号查询库存批次最近5条过期日期+批次号(入库业务单新增页面下拉回显)
*/
public List<MaterialInventoryExpiryBatchPO> selectRecentExpiryBatches(String materialCode, Long topOrganizationId) {
return materialInventoryService.selectRecentExpiryBatches(materialCode, topOrganizationId);
}
/**
* @description:库存查询列表
* @param materialInventoryQueryListDO
@@ -21,20 +21,47 @@ import java.util.Map;
/**
* 维他奶出库Excel解析监听器新模板版
* 新模板特点货品明细与客户配送明细左右分栏展示
* 核心功能
* 1. 左侧栏第1-3行解析表头送货日期/送货线号/车次第5行解析纪录合共第7行起解析货品明细
* 2. 右侧栏第2行解析发票张数/小计第4行起解析客户配送明细
* 3. 自动处理文本型数字转换StringInteger/BigDecimal
* 4. 完整的异常处理和日志记录
* 核心功能行列位置均不写死模板调整行/列顺序不影响解析
* 1. 表头标签送货日期/送货线号/车次/发票张数/小计/纪录合共按标签文字定位标签所在列取右侧相邻列的值
* 2. 货品明细按列头文字貨品編號/簡稱//支等繁简体归一化定位各列列头行之后的行按数据解析
* 3. 客户配送明细按列头文字類别/發票編號/客户編號/名稱///金額等定位各列
* 4. 列头行识别失败时兜底按老模板固定布局左栏A-D列右栏F-L列行号1-7解析保证老模板不受影响
* 5. 自动处理文本型数字转换StringInteger/BigDecimal
*/
public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<String, Object>> {
// 日志实例
private static final Logger LOGGER = LoggerFactory.getLogger(DynamicDeliveryExcelListener.class);
// 关键字定义Excel标签匹配
private static final String KEYWORD_TOTAL_RECORD = "紀錄合共";
private static final String KEYWORD_INVOICE_COUNT = "發票張數";
private static final String KEYWORD_SUBTOTAL = "小計";
// ---------------------- 关键字/列头别名定义归一化后匹配兼容简繁体 ----------------------
// 表头标签关键字
private static final String[] KEYWORD_DELIVERY_DATE = {"送货日期"};
private static final String[] KEYWORD_DELIVERY_LINE = {"送货线号", "送货线路"};
private static final String[] KEYWORD_TRAIN = {"车次"};
private static final String[] KEYWORD_TOTAL_RECORD = {"纪录合共"};
private static final String[] KEYWORD_INVOICE_COUNT = {"发票张数"};
private static final String[] KEYWORD_SUBTOTAL = {"小计"};
// 货品明细列头别名
private static final String[] PRODUCT_CODE_ALIASES = {"货品编号", "产品编号", "物料编号", "货品编码", "产品编码", "物料编码", "料号"};
private static final String[] PRODUCT_NAME_ALIASES = {"简称", "货品名称", "产品名称", "物料名称", "品名", "名称"};
private static final String[] PRODUCT_BOX_ALIASES = {"箱数", ""};
private static final String[] PRODUCT_PIECE_ALIASES = {"支数", "件数", ""};
// 客户配送明细列头别名
private static final String[] CATEGORY_ALIASES = {"类别"};
private static final String[] INVOICE_NO_ALIASES = {"发票编号", "发票号", "发票"};
private static final String[] CUSTOMER_CODE_ALIASES = {"客户编号", "客户编码", "客户代码"};
private static final String[] CUSTOMER_NAME_ALIASES = {"客户名称", "名称"};
private static final String[] CUSTOMER_BOX_ALIASES = {"箱数", ""};
private static final String[] CUSTOMER_PACKAGE_ALIASES = {"包数", ""};
private static final String[] CUSTOMER_AMOUNT_ALIASES = {"金额", "总额"};
// 列头行认定门槛锚点列编号列必须识别到且识别列总数达标防止数据行误判为列头行
private static final int HEADER_MIN_MATCH_COUNT = 2;
// 老模板兜底货品明细数据固定从第7行客户配送明细数据固定从第4行开始列头未识别到时启用固定列位
private static final int PRODUCT_DATA_START_ROW = 7;
private static final int CUSTOMER_DATA_START_ROW = 4;
// 支持的日期格式String转Date
private static final String[] SUPPORTED_DATE_FORMATS = {"yyyy-MM-dd", "yyyy/MM/dd", "yyyyMMdd", "yyyy-MM-dd HH:mm:ss"};
@@ -43,6 +70,26 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
private final List<ProductDetail> productDetailList = new ArrayList<>();
private final List<CustomerDeliveryDetail> customerDeliveryDetailList = new ArrayList<>();
// 货品明细列索引-1表示未识别该列
private int productCodeCol = -1;
private int productNameCol = -1;
private int productBoxCol = -1;
private int productPieceCol = -1;
private boolean productColumnsResolved = false;
// 客户配送明细列索引-1表示未识别该列
private int categoryCol = -1;
private int invoiceNoCol = -1;
private int customerCodeCol = -1;
private int customerNameCol = -1;
private int customerBoxCol = -1;
private int customerPackageCol = -1;
private int customerAmountCol = -1;
private boolean customerColumnsResolved = false;
// 发票张数/小计行是否已解析
private boolean invoiceRowParsed = false;
// 当前行号1-based与Excel行号一致
private int currentRowNum = 0;
@@ -53,10 +100,10 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
try {
LOGGER.debug("第{}行:开始解析,所有单元格数据:{}", currentRowNum, data);
// 左侧区域解析A-E列
// 左侧区域解析货品相关信息
parseLeftSection(data);
// 右侧区域解析F-M列
// 右侧区域解析客户配送相关信息
parseRightSection(data);
} catch (ExcelDataConvertException e) {
@@ -71,67 +118,66 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
// ---------------------- 左侧区域解析货品相关信息 ----------------------
private void parseLeftSection(Map<String, Object> data) {
switch (currentRowNum) {
case 1:
// 第1行送货日期A列=标签B列=
parseDeliveryDate(data);
break;
case 2:
// 第2行送货线号A列=标签B列=
parseDeliveryLineNo(data);
break;
case 3:
// 第3行车次A列=标签B列=
parseTrainNumber(data);
break;
case 4:
// 第4行货品明细标题行无需解析
LOGGER.debug("第{}行:货品明细标题行,跳过", currentRowNum);
break;
case 5:
// 第5行纪录合共A列=标签B列=总记录数D列=总箱数E列=总支数
parseTotalRecord(data);
break;
case 6:
// 第6行货品明细列标题貨品編號/簡稱//跳过
LOGGER.debug("第{}行:货品明细列标题行,跳过", currentRowNum);
break;
default:
// 第7行起货品明细数据行遇到空行自动结束
if (currentRowNum >= 7 && isValidProductDetailRow(data)) {
parseProductDetail(data, currentRowNum);
}
break;
// 1. 表头标签送货日期/送货线号/车次按标签文字定位行位置不限
parseHeaderLabels(data);
// 2. 纪录合共按标签文字定位行位置不限
parseTotalRecord(data);
// 3. 货品明细列布局已确定时按数据行解析
if (productColumnsResolved) {
if (isValidProductDetailRow(data)) {
parseProductDetail(data, currentRowNum);
}
return;
}
// 4. 尝试识别货品明细列头行识别成功则本行即列头行跳过数据解析
if (tryResolveProductColumns(data)) {
return;
}
// 5. 超过老模板数据起始行仍未识别到列头行启用固定列位兜底从当前行开始按数据行解析
if (currentRowNum >= PRODUCT_DATA_START_ROW) {
applyProductFallbackColumns();
if (isValidProductDetailRow(data)) {
parseProductDetail(data, currentRowNum);
}
}
}
// ---------------------- 右侧区域解析客户配送相关信息 ----------------------
private void parseRightSection(Map<String, Object> data) {
switch (currentRowNum) {
case 1:
// 第1行客户配送明细标题行无需解析
LOGGER.debug("第{}行:客户配送明细标题行,跳过", currentRowNum);
break;
case 2:
// 第2行发票张数/小计F列=發票張數标签G列=数值J列=小計标签K/L/M列=数值
parseInvoiceCount(data);
break;
case 3:
// 第3行客户配送明细列标题類别/發票編號/客户編號/名稱///金額跳过
LOGGER.debug("第{}行:客户配送明细列标题行,跳过", currentRowNum);
break;
default:
// 第4行起客户配送明细数据行遇到空行自动结束
if (currentRowNum >= 4 && isValidCustomerDetailRow(data)) {
parseCustomerDeliveryDetail(data, currentRowNum);
}
break;
// 1. 发票张数/小计按标签文字定位行位置不限解析成功后不再重复解析
if (!invoiceRowParsed) {
parseInvoiceRow(data);
}
// 2. 客户配送明细列布局已确定时按数据行解析
if (customerColumnsResolved) {
if (isValidCustomerDetailRow(data)) {
parseCustomerDeliveryDetail(data, currentRowNum);
}
return;
}
// 3. 尝试识别客户配送明细列头行识别成功则本行即列头行跳过数据解析
if (tryResolveCustomerColumns(data)) {
return;
}
// 4. 超过老模板数据起始行仍未识别到列头行启用固定列位兜底从当前行开始按数据行解析
if (currentRowNum >= CUSTOMER_DATA_START_ROW) {
applyCustomerFallbackColumns();
if (isValidCustomerDetailRow(data)) {
parseCustomerDeliveryDetail(data, currentRowNum);
}
}
}
// ---------------------- 解析完成回调 ----------------------
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 小计标签始终未识别到时保持与原逻辑一致小计相关字段置0
if (deliveryHeader.getSubtotalAmount() == null) {
LOGGER.warn("未找到【小计】标签,小计信息置空");
deliveryHeader.setSubtotalBoxCount(0);
deliveryHeader.setSubtotalPieceCount(0);
deliveryHeader.setSubtotalAmount(BigDecimal.ZERO);
}
LOGGER.info("Excel解析全部完成!最终统计结果:");
LOGGER.info("1. 表头信息:送货日期={},送货线号={},车次={},发票张数={},总记录数={},小计金额={}",
deliveryHeader.getDeliveryDate(),
@@ -144,47 +190,52 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
LOGGER.info("3. 客户配送明细:共{}条记录", customerDeliveryDetailList.size());
}
// ---------------------- 左侧表头信息解析 ----------------------
private void parseDeliveryDate(Map<String, Object> data) {
Object value = getValueSafely(data, 1); // B列
if (value != null) {
Date deliveryDate = convertToDate(value);
deliveryHeader.setDeliveryDate(deliveryDate);
LOGGER.debug("第{}行:送货日期解析完成,值={}", currentRowNum, deliveryDate);
// ---------------------- 左侧表头信息解析标签定位行位置不限 ----------------------
private void parseHeaderLabels(Map<String, Object> data) {
// 送货日期
if (deliveryHeader.getDeliveryDate() == null) {
Integer keywordIndex = findKeywordIndex(data, KEYWORD_DELIVERY_DATE);
if (keywordIndex != null) {
Date deliveryDate = convertToDate(getValueSafely(data, keywordIndex + 1));
if (deliveryDate != null) {
deliveryHeader.setDeliveryDate(deliveryDate);
LOGGER.debug("第{}行:送货日期解析完成,值={}", currentRowNum, deliveryDate);
}
}
}
// 送货线号
if (deliveryHeader.getDeliveryLineNo() == null || deliveryHeader.getDeliveryLineNo().trim().isEmpty()) {
Integer keywordIndex = findKeywordIndex(data, KEYWORD_DELIVERY_LINE);
if (keywordIndex != null) {
Object value = getValueSafely(data, keywordIndex + 1);
if (value != null && !value.toString().trim().isEmpty()) {
deliveryHeader.setDeliveryLineNo(value.toString().trim());
LOGGER.debug("第{}行:送货线号解析完成,值={}", currentRowNum, deliveryHeader.getDeliveryLineNo());
}
}
}
// 车次
if (deliveryHeader.getTrainNumber() == null) {
Integer keywordIndex = findKeywordIndex(data, KEYWORD_TRAIN);
if (keywordIndex != null) {
deliveryHeader.setTrainNumber(convertToInteger(getValueSafely(data, keywordIndex + 1)));
LOGGER.debug("第{}行:车次解析完成,值={}", currentRowNum, deliveryHeader.getTrainNumber());
}
}
}
private void parseDeliveryLineNo(Map<String, Object> data) {
Object value = getValueSafely(data, 1); // B列
if (value != null) {
String deliveryLineNo = value.toString().trim();
deliveryHeader.setDeliveryLineNo(deliveryLineNo);
LOGGER.debug("第{}行:送货线号解析完成,值={}", currentRowNum, deliveryLineNo);
}
}
private void parseTrainNumber(Map<String, Object> data) {
Object value = getValueSafely(data, 1); // B列
Integer trainNumber = convertToInteger(value);
deliveryHeader.setTrainNumber(trainNumber);
LOGGER.debug("第{}行:车次解析完成,值={}", currentRowNum, trainNumber);
}
// ---------------------- 左侧纪录合共解析 ----------------------
// ---------------------- 左侧纪录合共解析标签定位行位置不限 ----------------------
/**
* 第5行纪录合共
* 列对应关系0=A列=紀錄合共标签1=B列=总记录数3=D列=总箱数4=E列=总支数
* 纪录合共标签右侧第1列=总记录数第3列=总箱数第4列=总支数与老模板B/D/E列偏移一致
*/
private void parseTotalRecord(Map<String, Object> data) {
Object totalRecordLabel = getValueSafely(data, 0);
if (totalRecordLabel == null || !totalRecordLabel.toString().trim().contains(KEYWORD_TOTAL_RECORD)) {
LOGGER.warn("第{}行:不是纪录合共行,跳过解析,第一列内容:{}", currentRowNum, totalRecordLabel);
Integer keywordIndex = findKeywordIndex(data, KEYWORD_TOTAL_RECORD);
if (keywordIndex == null) {
return;
}
Integer totalRecordCount = convertToInteger(getValueSafely(data, 1)); // B列
Integer totalBoxCount = convertToInteger(getValueSafely(data, 3)); // D列
Integer totalPieceCount = convertToInteger(getValueSafely(data, 4)); // E列
Integer totalRecordCount = convertToInteger(getValueSafely(data, keywordIndex + 1));
Integer totalBoxCount = convertToInteger(getValueSafely(data, keywordIndex + 3));
Integer totalPieceCount = convertToInteger(getValueSafely(data, keywordIndex + 4));
deliveryHeader.setTotalRecordCount(totalRecordCount);
deliveryHeader.setTotalBoxCount(totalBoxCount);
@@ -194,27 +245,66 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
currentRowNum, totalRecordCount, totalBoxCount, totalPieceCount);
}
// ---------------------- 左侧货品明细解析 ----------------------
// ---------------------- 左侧货品明细列头识别与数据解析 ----------------------
/**
* 校验货品明细行是否有效排除表头和空行
* 尝试将当前行识别为货品明细列头行以货品编号列为锚点按列头文字简繁归一化匹配各列
*/
private boolean isValidProductDetailRow(Map<String, Object> data) {
Object productCode = getValueSafely(data, 0); // A列
return productCode != null
&& !productCode.toString().trim().isEmpty()
&& !"貨品編號".equals(productCode.toString().trim());
private boolean tryResolveProductColumns(Map<String, Object> data) {
int codeCol = findColumnByAliases(data, PRODUCT_CODE_ALIASES);
if (codeCol < 0) {
return false;
}
int nameCol = findColumnByAliases(data, PRODUCT_NAME_ALIASES);
int boxCol = findColumnByAliases(data, PRODUCT_BOX_ALIASES);
int pieceCol = findColumnByAliases(data, PRODUCT_PIECE_ALIASES);
int matched = 1 + (nameCol >= 0 ? 1 : 0) + (boxCol >= 0 ? 1 : 0) + (pieceCol >= 0 ? 1 : 0);
if (matched < HEADER_MIN_MATCH_COUNT) {
return false;
}
productCodeCol = codeCol;
productNameCol = nameCol;
productBoxCol = boxCol;
productPieceCol = pieceCol;
productColumnsResolved = true;
LOGGER.info("第{}行识别为货品明细列头行,列映射:货品编号={},名称={},箱={},支={}",
currentRowNum, productCodeCol, productNameCol, productBoxCol, productPieceCol);
return true;
}
/**
* 解析货品明细第7行起
* 列对应关系0=A列=貨品編號1=B列=簡稱2=C列=3=D列=
* 兜底列位老模板货品明细固定布局0=A列=貨品編號1=B列=簡稱2=C列=3=D列=
*/
private void applyProductFallbackColumns() {
productCodeCol = 0;
productNameCol = 1;
productBoxCol = 2;
productPieceCol = 3;
productColumnsResolved = true;
LOGGER.warn("第{}行前未识别到货品明细列头行,启用老模板固定列位兜底解析", currentRowNum);
}
/**
* 校验货品明细行是否有效排除列头行和空行
*/
private boolean isValidProductDetailRow(Map<String, Object> data) {
Object productCode = getValueSafely(data, productCodeCol);
if (productCode == null || productCode.toString().trim().isEmpty()) {
return false;
}
// 列头文字本身不算数据行防止列头行被当作数据重复解析
String text = normalizeHeaderText(productCode);
return !matchesAny(text, PRODUCT_CODE_ALIASES);
}
/**
* 解析货品明细列头行之后的行
*/
private void parseProductDetail(Map<String, Object> data, int rowNum) {
ProductDetail productDetail = new ProductDetail();
productDetail.setProductCode(convertToString(getValueSafely(data, 0)));
productDetail.setProductName(convertToString(getValueSafely(data, 1)));
productDetail.setBoxCount(convertToInteger(getValueSafely(data, 2)));
productDetail.setPieceCount(convertToInteger(getValueSafely(data, 3)));
productDetail.setProductCode(convertToString(getValueSafely(data, productCodeCol)));
productDetail.setProductName(convertToString(getValueSafely(data, productNameCol)));
productDetail.setBoxCount(convertToInteger(getValueSafely(data, productBoxCol)));
productDetail.setPieceCount(convertToInteger(getValueSafely(data, productPieceCol)));
productDetailList.add(productDetail);
LOGGER.debug("第{}行:解析货品明细完成,货品编号={},名称={},箱数={},支数={}",
@@ -222,67 +312,105 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
productDetail.getBoxCount(), productDetail.getPieceCount());
}
// ---------------------- 右侧发票张数与小计解析 ----------------------
/**
* 第2行发票张数与小计
* 通过关键字定位标签再读取右侧相邻列避免空列导致索引偏移
*/
private void parseInvoiceCount(Map<String, Object> data) {
// 1. 定位并解析发票张数
Integer invoiceCount = findValueAfterKeyword(data, KEYWORD_INVOICE_COUNT);
if (invoiceCount != null) {
deliveryHeader.setInvoiceCount(invoiceCount);
// ---------------------- 右侧发票张数与小计解析标签定位行位置不限 ----------------------
private void parseInvoiceRow(Map<String, Object> data) {
// 1. 发票张数标签右侧相邻列
Integer invoiceIndex = findKeywordIndex(data, KEYWORD_INVOICE_COUNT);
if (invoiceIndex != null) {
deliveryHeader.setInvoiceCount(convertToInteger(getValueSafely(data, invoiceIndex + 1)));
}
// 2. 定位并解析小计标签右侧第1列=第2列=第3列=金额
Integer subtotalStartIndex = findKeywordIndex(data, KEYWORD_SUBTOTAL);
if (subtotalStartIndex != null && subtotalStartIndex >= 0) {
Integer subtotalBoxCount = convertToInteger(getValueSafely(data, subtotalStartIndex + 1));
Integer subtotalPieceCount = convertToInteger(getValueSafely(data, subtotalStartIndex + 2));
BigDecimal subtotalAmount = convertToBigDecimal(getValueSafely(data, subtotalStartIndex + 3));
// 2. 小计标签右侧第1列=第2列=第3列=金额
Integer subtotalIndex = findKeywordIndex(data, KEYWORD_SUBTOTAL);
if (subtotalIndex != null && subtotalIndex >= 0) {
Integer subtotalBoxCount = convertToInteger(getValueSafely(data, subtotalIndex + 1));
Integer subtotalPieceCount = convertToInteger(getValueSafely(data, subtotalIndex + 2));
BigDecimal subtotalAmount = convertToBigDecimal(getValueSafely(data, subtotalIndex + 3));
deliveryHeader.setSubtotalBoxCount(subtotalBoxCount);
deliveryHeader.setSubtotalPieceCount(subtotalPieceCount);
deliveryHeader.setSubtotalAmount(subtotalAmount);
invoiceRowParsed = true;
LOGGER.debug("第{}行:解析小计完成,箱数={},支数={},金额={}",
currentRowNum, subtotalBoxCount, subtotalPieceCount, subtotalAmount);
} else {
LOGGER.warn("第{}行:未找到小计标签,小计信息置空", currentRowNum);
deliveryHeader.setSubtotalBoxCount(0);
deliveryHeader.setSubtotalPieceCount(0);
deliveryHeader.setSubtotalAmount(BigDecimal.ZERO);
LOGGER.debug("第{}行:解析发票张数行完成,发票张数={}小计箱数={},支数={},金额={}",
currentRowNum, deliveryHeader.getInvoiceCount(), subtotalBoxCount, subtotalPieceCount, subtotalAmount);
}
LOGGER.debug("第{}行:解析发票张数行完成,发票张数={},小计金额={}",
currentRowNum, deliveryHeader.getInvoiceCount(), deliveryHeader.getSubtotalAmount());
}
// ---------------------- 右侧客户配送明细解析 ----------------------
// ---------------------- 右侧客户配送明细列头识别与数据解析 ----------------------
/**
* 校验客户配送明细行是否有效排除表头和空行
* 尝试将当前行识别为客户配送明细列头行以客户编号列为锚点按列头文字简繁归一化匹配各列
*/
private boolean tryResolveCustomerColumns(Map<String, Object> data) {
int codeCol = findColumnByAliases(data, CUSTOMER_CODE_ALIASES);
if (codeCol < 0) {
return false;
}
int categoryColTmp = findColumnByAliases(data, CATEGORY_ALIASES);
int invoiceNoColTmp = findColumnByAliases(data, INVOICE_NO_ALIASES);
int nameColTmp = findColumnByAliases(data, CUSTOMER_NAME_ALIASES);
int boxColTmp = findColumnByAliases(data, CUSTOMER_BOX_ALIASES);
int packageColTmp = findColumnByAliases(data, CUSTOMER_PACKAGE_ALIASES);
int amountColTmp = findColumnByAliases(data, CUSTOMER_AMOUNT_ALIASES);
int matched = 1 + (categoryColTmp >= 0 ? 1 : 0) + (invoiceNoColTmp >= 0 ? 1 : 0) + (nameColTmp >= 0 ? 1 : 0)
+ (boxColTmp >= 0 ? 1 : 0) + (packageColTmp >= 0 ? 1 : 0) + (amountColTmp >= 0 ? 1 : 0);
if (matched < HEADER_MIN_MATCH_COUNT) {
return false;
}
categoryCol = categoryColTmp;
invoiceNoCol = invoiceNoColTmp;
customerCodeCol = codeCol;
customerNameCol = nameColTmp;
customerBoxCol = boxColTmp;
customerPackageCol = packageColTmp;
customerAmountCol = amountColTmp;
customerColumnsResolved = true;
LOGGER.info("第{}行识别为客户配送明细列头行,列映射:类别={},发票编号={},客户编号={},名称={},箱={},包={},金额={}",
currentRowNum, categoryCol, invoiceNoCol, customerCodeCol, customerNameCol, customerBoxCol, customerPackageCol, customerAmountCol);
return true;
}
/**
* 兜底列位老模板客户配送明细固定布局5=F列=類别6=G列=發票編號7=H列=客户編號8=I列=名稱
* 9=J列=10=K列=11=L列=金額
*/
private void applyCustomerFallbackColumns() {
categoryCol = 5;
invoiceNoCol = 6;
customerCodeCol = 7;
customerNameCol = 8;
customerBoxCol = 9;
customerPackageCol = 10;
customerAmountCol = 11;
customerColumnsResolved = true;
LOGGER.warn("第{}行前未识别到客户配送明细列头行,启用老模板固定列位兜底解析", currentRowNum);
}
/**
* 校验客户配送明细行是否有效排除列头行和空行
*/
private boolean isValidCustomerDetailRow(Map<String, Object> data) {
Object category = getValueSafely(data, 5); // F列
return category != null
&& !category.toString().trim().isEmpty()
&& !"類别".equals(category.toString().trim());
Object category = getValueSafely(data, categoryCol);
if (category == null || category.toString().trim().isEmpty()) {
return false;
}
// 列头文字本身不算数据行防止列头行被当作数据重复解析
String text = normalizeHeaderText(category);
return !matchesAny(text, CATEGORY_ALIASES);
}
/**
* 解析客户配送明细第4行起
* 列对应关系5=F列=類别6=G列=發票編號7=H列=客户編號8=I列=名稱
* 9=J列=10=K列=11=L列=金額
* 解析客户配送明细列头行之后的行
*/
private void parseCustomerDeliveryDetail(Map<String, Object> data, int rowNum) {
CustomerDeliveryDetail customerDetail = new CustomerDeliveryDetail();
customerDetail.setCategory(convertToString(getValueSafely(data, 5)));
customerDetail.setInvoiceNo(convertToString(getValueSafely(data, 6)));
customerDetail.setCustomerCode(convertToString(getValueSafely(data, 7)));
customerDetail.setCustomerName(convertToString(getValueSafely(data, 8)));
customerDetail.setBoxCount(convertToInteger(getValueSafely(data, 9)));
customerDetail.setPackageCount(convertToInteger(getValueSafely(data, 10)));
customerDetail.setAmount(convertToBigDecimal(getValueSafely(data, 11)));
customerDetail.setCategory(convertToString(getValueSafely(data, categoryCol)));
customerDetail.setInvoiceNo(convertToString(getValueSafely(data, invoiceNoCol)));
customerDetail.setCustomerCode(convertToString(getValueSafely(data, customerCodeCol)));
customerDetail.setCustomerName(convertToString(getValueSafely(data, customerNameCol)));
customerDetail.setBoxCount(convertToInteger(getValueSafely(data, customerBoxCol)));
customerDetail.setPackageCount(convertToInteger(getValueSafely(data, customerPackageCol)));
customerDetail.setAmount(convertToBigDecimal(getValueSafely(data, customerAmountCol)));
customerDeliveryDetailList.add(customerDetail);
LOGGER.debug("第{}行:解析客户配送明细完成,类别={},发票编号={},客户编号={},名称={},箱数={},包数={},金额={}",
@@ -299,7 +427,7 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
* 此时key不是数字字符串需跳过这些key走位置兜底逻辑
*/
private Object getValueSafely(Map<String, Object> data, int columnIndex) {
if (data == null || data.isEmpty()) {
if (columnIndex < 0 || data == null || data.isEmpty()) {
return null;
}
@@ -333,9 +461,9 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
return "未知列";
}
int index = 0;
for (String key : data.keySet()) {
for (Object key : data.keySet()) {
if (index == columnIndex) {
return key;
return String.valueOf(key);
}
index++;
}
@@ -343,22 +471,28 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
}
/**
* 在指定行中查找关键字所在的列索引
* 注意Map的key可能是非数字字符串如表头文本"客户配送明细"此时跳过该key不抛出异常
* 在指定行中查找关键字所在的列索引关键字归一化后包含匹配兼容简繁体与冒号等符号
* 注意Map的key可能是非数字字符串如表头文本此时跳过该key不抛出异常
*/
private Integer findKeywordIndex(Map<String, Object> data, String keyword) {
if (data == null || data.isEmpty() || keyword == null) {
private Integer findKeywordIndex(Map<String, Object> data, String[] keywords) {
if (data == null || data.isEmpty() || keywords == null) {
return null;
}
for (Object keyObj : data.keySet()) {
Object value = data.get(keyObj);
if (value != null && value.toString().trim().contains(keyword)) {
if (value == null) {
continue;
}
String text = normalizeHeaderText(value);
if (text.isEmpty()) {
continue;
}
if (matchesAny(text, keywords)) {
try {
return Integer.parseInt(keyObj.toString());
} catch (NumberFormatException e) {
// key不是数字可能是表头文本"客户配送明细"跳过不打印警告避免日志刷屏
LOGGER.debug("第{}行:关键字'{}'所在列key不是数字,key={},跳过",
currentRowNum, keyword, keyObj);
// key不是数字可能是表头文本跳过不打印警告避免日志刷屏
LOGGER.debug("第{}行:关键字所在列key不是数字,key={},跳过", currentRowNum, keyObj);
}
}
}
@@ -366,15 +500,64 @@ public class DynamicDeliveryExcelListener extends AnalysisEventListener<Map<Stri
}
/**
* 查找关键字所在列的右侧相邻列值并转为Integer
* 在指定行中按列头别名查找列索引列头文字归一化后包含匹配返回首个命中的列未命中返回-1
*/
private Integer findValueAfterKeyword(Map<String, Object> data, String keyword) {
Integer keywordIndex = findKeywordIndex(data, keyword);
if (keywordIndex == null) {
LOGGER.warn("第{}行:未找到关键字'{}'", currentRowNum, keyword);
return null;
private int findColumnByAliases(Map<String, Object> data, String[] aliases) {
if (data == null || data.isEmpty() || aliases == null) {
return -1;
}
return convertToInteger(getValueSafely(data, keywordIndex + 1));
for (Object keyObj : data.keySet()) {
Object value = data.get(keyObj);
if (value == null) {
continue;
}
String text = normalizeHeaderText(value);
if (text.isEmpty()) {
continue;
}
if (matchesAny(text, aliases)) {
try {
return Integer.parseInt(keyObj.toString());
} catch (NumberFormatException e) {
// key不是数字可能是表头文本跳过
LOGGER.debug("第{}行:列头所在列key不是数字,key={},跳过", currentRowNum, keyObj);
}
}
}
return -1;
}
/**
* 列头/标签文字归一化去空格/冒号/括号等符号转小写繁体转简体覆盖模板常见用字
*/
private String normalizeHeaderText(Object value) {
if (value == null) {
return "";
}
String s = value.toString().trim().toLowerCase();
s = s.replace("", "").replace(":", "")
.replace(" ", "").replace(" ", "")
.replace("(", "").replace(")", "")
.replace("", "").replace("", "")
.replace("/", "").replace("\\", "");
s = s.replace('貨', '货').replace('編', '编').replace('號', '号').replace('稱', '称')
.replace('數', '数').replace('飲', '饮').replace('規', '规').replace('戶', '户')
.replace('紀', '纪').replace('錄', '录').replace('張', '张').replace('計', '计')
.replace('車', '车').replace('線', '线').replace('類', '类').replace('別', '别')
.replace('簡', '简').replace('產', '产').replace('單', '单');
return s;
}
/**
* 判断归一化后的文字是否命中任一别名
*/
private boolean matchesAny(String text, String[] aliases) {
for (String alias : aliases) {
if (text.contains(alias)) {
return true;
}
}
return false;
}
/**
@@ -17,7 +17,11 @@ import java.util.Map;
/**
* 入库单Excel解析监听器
* 功能1. 解析第一行表头数据 2. 跳过第二行合计行第三行列名行 3. 解析第四行开始的产品明细 4. 支持文本型数字转换
* 功能1. 解析第一行表头数据入库日期OrderNO按标签定位位置不敏感
* 2. 动态定位列头行扫描前几行识别出"產品編號"等已知列头文字的行即认定为列头行行位置不敏感
* 3. 列按列头文字动态定位兼容繁体列头產品編號/客戶編號/件數/飲用日期等列顺序调整不影响解析
* 4. 列头无法识别时兜底按老模板固定列位0=编号1=名称2=规格3=件数4=饮用日期
* 5. 支持文本型数字转换
*/
public class StockInExcelListener extends AnalysisEventListener<Map<String, Object>> {
private static final Logger LOGGER = LoggerFactory.getLogger(StockInExcelListener.class);
@@ -35,6 +39,29 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
"dd.MM.yyyy", "d-MMM-yyyy", "yyyyMMddHHmmss"
};
// ====== 列头别名归一化后匹配兼容简繁体及常见叫法 ======
private static final String[] PRODUCT_CODE_ALIASES = {"产品编号", "物料编号", "产品编码", "物料编码", "料号"};
private static final String[] PRODUCT_NAME_ALIASES = {"产品名称", "物料名称", "商品名称", "品名"};
private static final String[] CUSTOMER_CODE_ALIASES = {"客户编号", "客户编码", "客户代码"};
private static final String[] DRINK_DATE_ALIASES = {"饮用日期", "饮用时间"};
private static final String[] PIECE_COUNT_ALIASES = {"件数"};
private static final String[] SPEC_ALIASES = {"规格", "箱含量"};
// 列头行至少需识别出的列数防止把日期标签行/合计行误判为列头行
private static final int HEADER_MIN_MATCH_COUNT = 2;
// 老模板列头行固定在第3行超过该行仍未识别到列头行时启用固定列位兜底
private static final int FALLBACK_AFTER_ROW_NUM = 3;
// 动态解析出的列索引-1表示未识别该列
private int productCodeCol = -1;
private int productNameCol = -1;
private int customerCodeCol = -1;
private int drinkDateCol = -1;
private int pieceCountCol = -1;
private int specCol = -1;
// 列布局是否已确定列头行识别成功或已启用兜底
private boolean columnResolved = false;
// 行号标记用于区分第一行和表格行
private int currentRowNum = 0;
@@ -48,21 +75,21 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
if (currentRowNum == 1) {
parseHeaderData(data);
}
// 2. 跳过第二行合计行Total: xxx
else if (currentRowNum == 2) {
LOGGER.debug("第{}行是合计行,跳过解析", currentRowNum);
}
// 3. 跳过第三行表格列名行產品編號產品名稱...
else if (currentRowNum == 3) {
LOGGER.debug("第{}行是表格列名行,跳过解析", currentRowNum);
}
// 4. 解析第四行及以后产品明细
else if (currentRowNum >= 4) {
if (isValidProductRow(data)) {
StockInProductDetail detail = parseProductDetail(data);
productDetailList.add(detail);
// 2. 列布局未确定尝试识别列头行识别成功则本行即为列头行跳过数据解析
else if (!columnResolved) {
if (!tryResolveHeaderRow(data)) {
// 3. 超过老模板列头行位置仍未识别到列头行启用固定列位兜底从当前行开始按数据行解析
if (currentRowNum > FALLBACK_AFTER_ROW_NUM) {
applyFallbackColumns();
parseDataRow(data);
}
// 否则合计行Total等跳过
}
}
// 4. 列布局已确定按数据行解析
else {
parseDataRow(data);
}
} catch (ExcelDataConvertException e) {
// Excel内置转换异常如日期格式错误
@@ -79,6 +106,16 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
}
}
/**
* 数据行解析排除合计行/空行后解析产品明细
*/
private void parseDataRow(Map<String, Object> data) {
if (isValidProductRow(data)) {
StockInProductDetail detail = parseProductDetail(data);
productDetailList.add(detail);
}
}
/**
* 解析第一行表头数据
* 模板中标签与值相邻分别搜索"入库日期""OrderNO"标签后一列的值兼容位置调整
@@ -86,15 +123,17 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
private void parseHeaderData(Map<String, Object> data) {
// 入库日期查找包含"入库日期"标签的下一个值
Object stockInDateObj = getValueAfterLabel(data, "入庫日期:");
if (stockInDateObj == null) {
stockInDateObj = getValueAfterLabel(data, "入库日期");
}
if (stockInDateObj != null) {
Date stockInDate = convertToDate(stockInDateObj);
stockInHeader.setStockInDate(stockInDate);
LOGGER.debug("第1行:入库日期解析完成,值:{}", stockInDate);
}
// OrderNO优先查找"OrderNO"标签后的值老模板布局标签与值相邻
// 新模板第1行无"OrderNO"标签单号值直接放在单元格中(如E1)
// 此时取第1行中除标签入库日期以外的第一个非空字符串值
// OrderNO优先查找"OrderNO"标签后的值标签与值相邻
// 新模板第1行无"OrderNO"标签取第1行中除标签入库日期以外的第一个非空字符串值
Object orderNoObj = getValueAfterLabel(data, "OrderNO");
if (orderNoObj == null) {
for (Object value : data.values()) {
@@ -103,7 +142,7 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
}
String str = value.toString().trim();
// 跳过标签单元格与已被识别为入库日期的值
if (str.isEmpty() || str.contains("入庫日期") || str.contains("OrderNO") || str.contains("Total")) {
if (str.isEmpty() || str.contains("入庫日期") || str.contains("入库日期") || str.contains("OrderNO") || str.contains("Total")) {
continue;
}
if (value == stockInDateObj) {
@@ -125,26 +164,113 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
}
}
/**
* 尝试将当前行识别为列头行按归一化后的列头文字匹配已知列别名
* 识别成功至少识别出产品编号列且总数达标返回true并记录各列索引
*/
private boolean tryResolveHeaderRow(Map<String, Object> data) {
if (data == null || data.isEmpty()) {
return false;
}
int tmpProductCodeCol = -1;
int tmpProductNameCol = -1;
int tmpCustomerCodeCol = -1;
int tmpDrinkDateCol = -1;
int tmpPieceCountCol = -1;
int tmpSpecCol = -1;
int matched = 0;
for (Object keyObj : data.keySet()) {
Object value = data.get(keyObj);
if (value == null) {
continue;
}
String text = normalizeHeaderText(value);
if (text.isEmpty()) {
continue;
}
int colIndex;
try {
colIndex = Integer.parseInt(keyObj.toString());
} catch (NumberFormatException e) {
// key不是数字如表头文本跳过
continue;
}
// 按组依次匹配先具体后一般避免同一列头命中多组别名时归属错乱
if (tmpProductCodeCol < 0 && matchesAny(text, PRODUCT_CODE_ALIASES)) {
tmpProductCodeCol = colIndex;
matched++;
} else if (tmpCustomerCodeCol < 0 && matchesAny(text, CUSTOMER_CODE_ALIASES)) {
tmpCustomerCodeCol = colIndex;
matched++;
} else if (tmpProductNameCol < 0 && matchesAny(text, PRODUCT_NAME_ALIASES)) {
tmpProductNameCol = colIndex;
matched++;
} else if (tmpDrinkDateCol < 0 && matchesAny(text, DRINK_DATE_ALIASES)) {
tmpDrinkDateCol = colIndex;
matched++;
} else if (tmpPieceCountCol < 0 && matchesAny(text, PIECE_COUNT_ALIASES)) {
tmpPieceCountCol = colIndex;
matched++;
} else if (tmpSpecCol < 0 && matchesAny(text, SPEC_ALIASES)) {
tmpSpecCol = colIndex;
matched++;
}
}
// 产品编号列是数据解析的锚点必须识别到且识别总数达标才认定为列头行
if (tmpProductCodeCol < 0 || matched < HEADER_MIN_MATCH_COUNT) {
return false;
}
productCodeCol = tmpProductCodeCol;
productNameCol = tmpProductNameCol;
customerCodeCol = tmpCustomerCodeCol;
drinkDateCol = tmpDrinkDateCol;
pieceCountCol = tmpPieceCountCol;
specCol = tmpSpecCol;
columnResolved = true;
LOGGER.info("第{}行识别为列头行,列映射:产品编号={},产品名称={},客户编号={},规格={},件数={},饮用日期={}",
currentRowNum, productCodeCol, productNameCol, customerCodeCol, specCol, pieceCountCol, drinkDateCol);
return true;
}
/**
* 兜底列位老模板固定布局0=编号1=名称2=规格3=件数4=饮用日期无客户编号列
*/
private void applyFallbackColumns() {
productCodeCol = 0;
productNameCol = 1;
customerCodeCol = -1;
specCol = 2;
pieceCountCol = 3;
drinkDateCol = 4;
columnResolved = true;
LOGGER.warn("第{}行前未识别到列头行,启用老模板固定列位兜底解析", currentRowNum);
}
/**
* 解析产品明细数据一行对应一个产品
* 新模板列顺序0=产品编号1=产品名称2=规格箱含量3=件数4=饮用日期
* 各列按列头行动态定位的索引取值未识别的列取null/默认值
*/
private StockInProductDetail parseProductDetail(Map<String, Object> data) {
StockInProductDetail detail = new StockInProductDetail();
// 产品编号第1列索引0- 支持文本型数字转换
detail.setProductCode(convertToString(getValueSafely(data, 0)));
// 产品名称第2列索引1
detail.setProductName(convertToString(getValueSafely(data, 1)));
// 箱含量/规格第3列索引2
detail.setBoxCapacity(convertToInteger(getValueSafely(data, 2)));
// 件数第4列索引3
detail.setPieceCount(convertToInteger(getValueSafely(data, 3)));
// 饮用日期第5列索引4
detail.setDrinkDate(convertToDate(getValueSafely(data, 4)));
// 产品编号可不填名称导入时按编号自动匹配库中物料名称
detail.setProductCode(convertToString(getValueSafely(data, productCodeCol)));
// 产品名称列头未识别或单元格未填时为空串由服务端按编号自动匹配覆盖
detail.setProductName(convertToString(getValueSafely(data, productNameCol)));
// 客户编号新模板新增列可不填填写后由服务端校验客户存在性并自动匹配客户名称
detail.setCustomerCode(convertToString(getValueSafely(data, customerCodeCol)));
// 箱含量/规格
detail.setBoxCapacity(convertToInteger(getValueSafely(data, specCol)));
// 件数
detail.setPieceCount(convertToInteger(getValueSafely(data, pieceCountCol)));
// 饮用日期
detail.setDrinkDate(convertToDate(getValueSafely(data, drinkDateCol)));
LOGGER.debug("第{}行产品明细解析完成,产品编号:{},产品名称:{}",
currentRowNum, detail.getProductCode(), detail.getProductName());
LOGGER.debug("第{}行产品明细解析完成,产品编号:{},产品名称:{},客户编号:{}",
currentRowNum, detail.getProductCode(), detail.getProductName(), detail.getCustomerCode());
return detail;
}
@@ -152,9 +278,6 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
* 校验产品行是否有效排除空行和合计行
*/
private boolean isValidProductRow(Map<String, Object> data) {
// 有效条件产品编号不为空 + 不是合计行
Object productCodeObj = getValueSafely(data, 0);
// 排除合计行整行包含"Total"字样
if (containsAnyValue(data, "Total")) {
LOGGER.debug("第{}行是合计行,跳过解析", currentRowNum);
@@ -162,6 +285,7 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
}
// 排除空行产品编号为空
Object productCodeObj = getValueSafely(data, productCodeCol);
return productCodeObj != null && !productCodeObj.toString().trim().isEmpty();
}
@@ -178,13 +302,24 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
// ---------------------- 工具方法类型转换+安全取值 ----------------------
/**
* 按列索引安全获取值支持列索引越界处理
* 按列索引安全获取值
* 优先按Map的key直接查找适配EasyExcel以列号为key的情况避免空单元格导致的位置偏移
* 找不到再按位置顺序查找保持与原逻辑兼容
*/
private Object getValueSafely(Map<String, Object> data, int columnIndex) {
if (data == null || data.isEmpty()) {
if (columnIndex < 0 || data == null || data.isEmpty()) {
return null;
}
String strIndex = String.valueOf(columnIndex);
// 1. 直接按key查找key为String或Integer跳过非数字key
for (Object keyObj : data.keySet()) {
if (keyObj != null && strIndex.equals(keyObj.toString())) {
return data.get(keyObj);
}
}
// 2. 兜底按顺序位置查找
int index = 0;
for (Object value : data.values()) {
if (index == columnIndex) {
@@ -193,7 +328,7 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
index++;
}
LOGGER.warn("第{}行列索引{}超出范围(当前行共{}列),返回null",
LOGGER.warn("第{}行列索引{}超出当前行单元格数量({}列),返回null",
currentRowNum, columnIndex, data.size());
return null;
}
@@ -207,9 +342,9 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
}
int index = 0;
for (String key : data.keySet()) {
for (Object key : data.keySet()) {
if (index == columnIndex) {
return key;
return String.valueOf(key);
}
index++;
}
@@ -251,6 +386,37 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
return false;
}
/**
* 列头文字归一化去空格/冒号/括号等符号转小写繁体转简体覆盖模板常见用字
*/
private String normalizeHeaderText(Object value) {
if (value == null) {
return "";
}
String s = value.toString().trim().toLowerCase();
s = s.replace("", "").replace(":", "")
.replace(" ", "").replace(" ", "")
.replace("(", "").replace(")", "")
.replace("", "").replace("", "")
.replace("/", "").replace("\\", "");
s = s.replace('產', '产').replace('編', '编').replace('號', '号').replace('稱', '称')
.replace('數', '数').replace('飲', '饮').replace('規', '规').replace('戶', '户')
.replace('貨', '货').replace('單', '单').replace('類', '类').replace('倉', '仓');
return s;
}
/**
* 判断归一化后的列头文字是否命中任一别名
*/
private boolean matchesAny(String text, String[] aliases) {
for (String alias : aliases) {
if (text.contains(alias)) {
return true;
}
}
return false;
}
/**
* 通用转换Object转String
*/
@@ -347,4 +513,4 @@ public class StockInExcelListener extends AnalysisEventListener<Map<String, Obje
public List<StockInProductDetail> getProductDetailList() {
return productDetailList;
}
}
}
@@ -172,10 +172,10 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
String lineCode = deliveryHeader.getDeliveryLineNo();
Integer trainNumber = deliveryHeader.getTrainNumber();
if (StringUtils.isBlank(lineCode)) {
throw new Exception("Excel未识别到【送貨線號】(线路),请检查模板第2行后重新导入");
throw new Exception("Excel未识别到【送貨線號】(线路),请检查模板表头后重新导入");
}
if (trainNumber == null || trainNumber <= 0) {
throw new Exception("Excel未识别到【車次】,请检查模板第3行后重新导入");
throw new Exception("Excel未识别到【車次】,请检查模板表头后重新导入");
}
AjaxResult lineResult = wmsServiceFeign.listLineManagement(lineCode);
if (lineResult == null || lineResult.get(AjaxResult.DATA_TAG) == null) {
@@ -259,17 +259,64 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
List<Map<String,Object>> shipperIds = new ArrayList<>();
Set<Long> processedShipperIds = new HashSet<>();
// ====== 第一步物料编号存在性校验落库前先收集全部缺失编号不立即报错 ======
Map<String, MaterialBaseInfoPO> materialCache = new LinkedHashMap<>();
List<String> missingMaterials = new ArrayList<>();
for (ProductDetail productDetail : productDetailList) {
MaterialBaseInfoPO materialBaseInfoPO = stockInOrderMapper.getByCode(productDetail.getProductCode(),loginUser.getUserPo().getOrganizationId());
if (materialBaseInfoPO != null && materialBaseInfoPO.getShipperId() != null) {
Long shipperId = materialBaseInfoPO.getShipperId();
if (processedShipperIds.add(shipperId)) {
Map<String, Object> shipper = new HashMap<>();
shipper.put("shipperId", shipperId);
shipper.put("shipperName", materialBaseInfoPO.getShipperName());
shipper.put("shipperCode", materialBaseInfoPO.getShipperCode());
shipperIds.add(shipper);
}
String productCode = productDetail.getProductCode() == null ? "" : productDetail.getProductCode().trim();
if (productCode.isEmpty()) {
throw new Exception("出库明细存在未填写產品編號的行,请补全后重新导入");
}
if (materialCache.containsKey(productCode)) {
continue;
}
MaterialBaseInfoPO materialBaseInfoPO = stockInOrderMapper.getByCode(productCode, loginUser.getUserPo().getOrganizationId());
if (materialBaseInfoPO == null) {
missingMaterials.add(productCode);
} else {
materialCache.put(productCode, materialBaseInfoPO);
}
}
// ====== 第二步客户编号存在性校验落库前先收集全部缺失编号不立即报错 ======
Map<String, UserPo> customerCache = new HashMap<>();
List<String> missingCustomers = new ArrayList<>();
for (CustomerDeliveryDetail customerDeliveryDetail : customerDeliveryDetailList) {
String customerCode = customerDeliveryDetail.getCustomerCode() == null ? "" : customerDeliveryDetail.getCustomerCode().trim();
if (customerCode.isEmpty()) {
throw new Exception("客户配送明细存在未填写客户编号的行,请补全后重新导入");
}
if (customerCache.containsKey(customerCode)) {
continue;
}
UserPo customer = stockOutOrderMapper.queryCustomerByCode(customerCode, loginUser.getUserPo().getOrganizationId());
if (customer == null || customer.getUserId() == null) {
missingCustomers.add(customerCode);
} else {
customerCache.put(customerCode, customer);
}
}
// ====== 第三步物料/客户存在性校验全部完成后统一提示两类编号各自有哪些不存在 ======
if (!missingMaterials.isEmpty() || !missingCustomers.isEmpty()) {
List<String> problems = new ArrayList<>();
if (!missingMaterials.isEmpty()) {
problems.add("物料[" + String.join("", missingMaterials) + "]不存在,请先维护物料信息");
}
if (!missingCustomers.isEmpty()) {
problems.add("客户[" + String.join("", missingCustomers) + "]不存在,请先维护客户信息");
}
throw new Exception(String.join("", problems) + ",维护完成后重新导入");
}
// ====== 后续处理编号均已存在货主归属校验建单落库 ======
for (MaterialBaseInfoPO materialBaseInfoPO : materialCache.values()) {
if (materialBaseInfoPO.getShipperId() != null && processedShipperIds.add(materialBaseInfoPO.getShipperId())) {
Map<String, Object> shipper = new HashMap<>();
shipper.put("shipperId", materialBaseInfoPO.getShipperId());
shipper.put("shipperName", materialBaseInfoPO.getShipperName());
shipper.put("shipperCode", materialBaseInfoPO.getShipperCode());
shipperIds.add(shipper);
}
}
@@ -330,7 +377,8 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
for (ProductDetail productDetail : productDetailList) {
ReservationOutMaterialDetailDO reservationOutMaterialDetailDO = new ReservationOutMaterialDetailDO();
reservationOutMaterialDetailDO.setOutOrderNumber(reservationStockOutOrderDO.getOutOrderNumber());
MaterialBaseInfoPO materialBaseInfoPO = stockInOrderMapper.getByCode(productDetail.getProductCode(),loginUser.getUserPo().getOrganizationId());
// 物料信息取自前置校验的缓存已确认存在避免逐行重复查库
MaterialBaseInfoPO materialBaseInfoPO = materialCache.get(productDetail.getProductCode());
reservationOutMaterialDetailDO.setQuantity(new BigDecimal(productDetail.getPieceCount()).setScale(2, BigDecimal.ROUND_HALF_UP));
if (materialBaseInfoPO != null) {
Long materialBaseInfoId = materialBaseInfoPO.getMaterialBaseInfoId();
@@ -398,9 +446,14 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
reservationDeliveryDetailsLink.setAmount(customerDeliveryDetail.getAmount());
reservationDeliveryDetailsLink.setCustomerNumber(customerDeliveryDetail.getCustomerCode());
reservationDeliveryDetailsLink.setDeliveryName(customerDeliveryDetail.getCustomerName());
String customerCode = customerDeliveryDetail.getCustomerCode();
Long userId = stockOutOrderMapper.queryShipperId(customerCode, loginUser.getUserPo().getOrganizationId());
reservationDeliveryDetailsLink.setCustomerId(userId);
String customerCode = customerDeliveryDetail.getCustomerCode() == null ? "" : customerDeliveryDetail.getCustomerCode().trim();
// 客户已在前置校验通过客户ID取库中匹配结果名称以库中数据为准自动匹配覆盖模板填写值库中名称为空时保留模板值
UserPo customer = customerCache.get(customerCode);
reservationDeliveryDetailsLink.setCustomerId(customer.getUserId());
if (customer.getUserName() != null && !customer.getUserName().trim().isEmpty()) {
customerDeliveryDetail.setCustomerName(customer.getUserName());
reservationDeliveryDetailsLink.setDeliveryName(customer.getUserName());
}
reservationDeliveryDetailsLink.setBox(customerDeliveryDetail.getBoxCount());
reservationDeliveryDetailsLink.setBag(customerDeliveryDetail.getPackageCount());
reservationDeliveryDetailsLink.setCategory(customerDeliveryDetail.getCategory());
@@ -462,30 +515,103 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
LOGGER.info("Excel解析成功,工作表:{},产品明细数:{}",
targetSheetName, resultDTO.getProductDetailList().size());
List<StockInProductDetail> productDetailList = resultDTO.getProductDetailList();
if (productDetailList == null || productDetailList.isEmpty()) {
throw new ServiceException("未解析到产品明细数据,请检查模板中是否填写了產品編號");
}
StockInHeader stockInHeader = resultDTO.getStockInHeader();
// ====== 第一步物料编号存在性校验模板只填物料编号即可名称按编号自动匹配 ======
// 查询结果为空说明库中没有该物料先收集全部缺失编号不立即报错
Map<String, MaterialBaseInfoPO> materialCache = new LinkedHashMap<>();
List<String> missingMaterials = new ArrayList<>();
for (StockInProductDetail productDetail : productDetailList) {
String productCode = productDetail.getProductCode();
if (materialCache.containsKey(productCode)) {
continue;
}
MaterialBaseInfoPO materialBaseInfoPO = stockInOrderMapper.getByCode(productCode, loginUser.getUserPo().getOrganizationId());
if (materialBaseInfoPO == null) {
missingMaterials.add(productCode);
} else {
materialCache.put(productCode, materialBaseInfoPO);
}
}
// ====== 第二步客户编号存在性校验先收集全部缺失编号不立即报错 ======
Set<String> customerCodes = new LinkedHashSet<>();
for (StockInProductDetail productDetail : productDetailList) {
if (productDetail.getCustomerCode() != null && !productDetail.getCustomerCode().trim().isEmpty()) {
customerCodes.add(productDetail.getCustomerCode().trim());
}
}
Map<String, UserPo> customerCache = new HashMap<>();
List<String> missingCustomers = new ArrayList<>();
for (String customerCode : customerCodes) {
UserPo customer = stockOutOrderMapper.queryCustomerByCode(customerCode, loginUser.getUserPo().getOrganizationId());
if (customer == null || customer.getUserId() == null) {
missingCustomers.add(customerCode);
} else {
customerCache.put(customerCode, customer);
}
}
// ====== 第三步物料/客户存在性校验全部完成后统一提示两类编号各自有哪些不存在 ======
if (!missingMaterials.isEmpty() || !missingCustomers.isEmpty()) {
List<String> problems = new ArrayList<>();
if (!missingMaterials.isEmpty()) {
problems.add("物料[" + String.join("", missingMaterials) + "]不存在,请先维护物料信息");
}
if (!missingCustomers.isEmpty()) {
problems.add("客户[" + String.join("", missingCustomers) + "]不存在,请先维护客户信息");
}
throw new ServiceException(String.join("", problems) + ",维护完成后重新导入");
}
// ====== 后续处理编号均已存在名称自动匹配货主归属与一致性校验建单 ======
// 明细回填库中匹配到的物料名称自动匹配模板中的名称列可不填/以库中为准库中名称为空时保留模板值
for (StockInProductDetail productDetail : productDetailList) {
String materialName = materialCache.get(productDetail.getProductCode()).getMaterialName();
if (materialName != null && !materialName.trim().isEmpty()) {
productDetail.setProductName(materialName);
}
}
// 物料归属货主集合用于客户编号一致性校验及未填客户编号时的兜底取值
List<Map<String,Object>> shipperIds = new ArrayList<>();
Set<Long> processedShipperIds = new HashSet<>();
for (StockInProductDetail productDetail : productDetailList) {
MaterialBaseInfoPO materialBaseInfoPO = stockInOrderMapper.getByCode(productDetail.getProductCode(),loginUser.getUserPo().getOrganizationId());
// 物料在当前登录组织下不存在时给出明确提示避免后续货主取值越界导致500
if (materialBaseInfoPO == null) {
throw new ServiceException("产品编号[" + productDetail.getProductCode() + "]在当前组织下不存在,请核对产品编号或使用物料所属组织的账号导入");
}
if (materialBaseInfoPO.getShipperId() != null) {
Long shipperId = materialBaseInfoPO.getShipperId();
if (processedShipperIds.add(shipperId)) {
Map<String, Object> shipper = new HashMap<>();
shipper.put("shipperId", shipperId);
shipper.put("shipperName", materialBaseInfoPO.getShipperName());
shipper.put("shipperCode", materialBaseInfoPO.getShipperCode());
shipperIds.add(shipper);
}
for (MaterialBaseInfoPO materialBaseInfoPO : materialCache.values()) {
if (materialBaseInfoPO.getShipperId() != null && processedShipperIds.add(materialBaseInfoPO.getShipperId())) {
Map<String, Object> shipper = new HashMap<>();
shipper.put("shipperId", materialBaseInfoPO.getShipperId());
shipper.put("shipperName", materialBaseInfoPO.getShipperName());
shipper.put("shipperCode", materialBaseInfoPO.getShipperCode());
shipperIds.add(shipper);
}
}
if (shipperIds.isEmpty()) {
throw new ServiceException("导入的产品均未配置货主,请先在物料档案中维护货主信息");
if (shipperIds.size() > 1) {
StringBuilder shipperCodeStr = new StringBuilder();
for (Map<String, Object> shipper : shipperIds) {
if (shipperCodeStr.length() > 0) {
shipperCodeStr.append("");
}
shipperCodeStr.append(shipper.get("shipperCode"));
}
throw new ServiceException("导入的物料不属于同一货主(货主编号:" + shipperCodeStr + "),一个入库单只能属于一个货主");
}
// 客户取值模板填了客户编号时取库中匹配结果编号/名称均为自动匹配值
UserPo customer = null;
if (customerCodes.size() > 1) {
throw new ServiceException("导入明细中存在多个客户编号(" + String.join("", customerCodes) + "),一个入库单只能属于一个客户");
}
if (!customerCodes.isEmpty()) {
String customerCode = customerCodes.iterator().next();
customer = customerCache.get(customerCode);
// 客户编号与物料归属货主一致性校验不一致拦截提示两者编号货主编码未维护时提示货主名称
if (!shipperIds.isEmpty() && !customer.getUserId().equals(shipperIds.get(0).get("shipperId"))) {
Object materialShipperCode = shipperIds.get(0).get("shipperCode");
String materialShipperDisplay = (materialShipperCode != null && !materialShipperCode.toString().trim().isEmpty())
? "货主编号[" + materialShipperCode + "]" : "货主[" + shipperIds.get(0).get("shipperName") + "]";
throw new ServiceException("客户编号[" + customerCode + "]与物料归属" + materialShipperDisplay + "]不一致,请核对后再导入");
}
stockInHeader.setCustomerCode(customerCode);
stockInHeader.setCustomerName(customer.getUserName());
}
ReservationStockInOrderDO reservationStockInOrderDO = new ReservationStockInOrderDO();
StockInHeader stockInHeader = resultDTO.getStockInHeader();
// 业务单号优先使用模板自带OrderNO不做格式规则校验与系统已有业务单号重复时提示不允许导入
// 入库业务单号(in_order_number)不使用模板单号统一由系统原本逻辑生成见insertDomain
// 模板未填单号时保持系统逻辑业务单号默认=入库业务单号
@@ -515,9 +641,19 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
reservationStockInOrderDO.setIssueStatus(0);
reservationStockInOrderDO.setSalesmanId(loginUser.getUserPo().getUserId().toString());
reservationStockInOrderDO.setSalesmanName(loginUser.getUserPo().getUserName());
reservationStockInOrderDO.setShipperId((Long) shipperIds.get(0).get("shipperId"));
reservationStockInOrderDO.setShipperName((String) shipperIds.get(0).get("shipperName"));
reservationStockInOrderDO.setShipperCode((String) shipperIds.get(0).get("shipperCode"));
// 单据货主模板填了客户编号时以客户为准编号/名称均为库中自动匹配值未填时取物料归属货主
if (customer != null) {
reservationStockInOrderDO.setShipperId(customer.getUserId());
reservationStockInOrderDO.setShipperName(customer.getUserName());
reservationStockInOrderDO.setShipperCode(customer.getUserMemberCode());
} else {
if (shipperIds.isEmpty()) {
throw new ServiceException("导入的产品均未配置货主,请先在物料档案中维护货主信息,或在模板中填写客户编号");
}
reservationStockInOrderDO.setShipperId((Long) shipperIds.get(0).get("shipperId"));
reservationStockInOrderDO.setShipperName((String) shipperIds.get(0).get("shipperName"));
reservationStockInOrderDO.setShipperCode((String) shipperIds.get(0).get("shipperCode"));
}
reservationStockInOrderDO.setWarehouseDate(stockInHeader.getStockInDate());
reservationStockInOrderDO.setStorageTime(stockInHeader.getStockInDate());
reservationStockInOrderDO.setExpectTime(stockInHeader.getStockInDate());
@@ -533,7 +669,8 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
reservationInMaterialDetailDO.setCreateBy(loginUser.getUserPo().getUserId());
reservationInMaterialDetailDO.setCreateByName(loginUser.getUserPo().getUserName());
reservationInMaterialDetailDO.setCreateTime(new Date());
MaterialBaseInfoPO materialBaseInfoPO = stockInOrderMapper.getByCode(stockInProductDetail.getProductCode(),loginUser.getUserPo().getOrganizationId());
// 物料信息取自前置校验的缓存已确认存在避免逐行重复查库
MaterialBaseInfoPO materialBaseInfoPO = materialCache.get(stockInProductDetail.getProductCode());
// 计划数量取模版"件数"同时件数字段落库用于明细列表回显
reservationInMaterialDetailDO.setQuantity(new BigDecimal(stockInProductDetail.getPieceCount()).setScale(2, BigDecimal.ROUND_HALF_UP));
reservationInMaterialDetailDO.setPieceCount(new BigDecimal(stockInProductDetail.getPieceCount()).setScale(2, BigDecimal.ROUND_HALF_UP));
@@ -14,6 +14,10 @@ public class StockInHeader {
private Date stockInDate;
// 订单编号Excel第一行第4列
private String orderNo;
// 客户编号明细列头动态定位导入校验客户存在性时回填
private String customerCode;
// 客户名称按客户编号自动匹配库中名称后回填
private String customerName;
// Getter和Setter方法
public Date getStockInDate() {
@@ -31,4 +35,20 @@ public class StockInHeader {
public void setOrderNo(String orderNo) {
this.orderNo = orderNo;
}
public String getCustomerCode() {
return customerCode;
}
public void setCustomerCode(String customerCode) {
this.customerCode = customerCode;
}
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
}
@@ -9,10 +9,12 @@ import java.util.Date;
*/
@Data
public class StockInProductDetail {
// 产品编号Excel第1列
// 产品编号按列头动态定位
private String productCode;
// 产品名称Excel第2列
// 产品名称按列头动态定位可不填导入时按产品编号自动匹配库中物料名称
private String productName;
// 客户编号按列头动态定位可不填导入时按客户编号自动匹配库中客户名称
private String customerCode;
// 箱含量Excel第3列
private Integer boxCapacity;
// 件数Excel第4列
@@ -37,6 +39,14 @@ public class StockInProductDetail {
this.productName = productName;
}
public String getCustomerCode() {
return customerCode;
}
public void setCustomerCode(String customerCode) {
this.customerCode = customerCode;
}
public Integer getBoxCapacity() {
return boxCapacity;
}
@@ -2,6 +2,7 @@ package com.mhd.oms.domain.reservationStockOutOrder.repository.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
//import com.mhd.oms.domain.reservationReservationStockOutOrder.entity.ReservationReservationStockOutOrder;
import com.mhd.common.core.domain.po.UserPo;
import com.mhd.oms.domain.reservationOutMaterialDetail.repository.po.ReservationOutMaterialDetailPO;
import com.mhd.oms.domain.reservationStockOutOrder.entity.ReservationStockOutOrder;
import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderPO;
@@ -36,7 +37,12 @@ public interface ReservationStockOutOrderMapper extends BaseMapper<ReservationSt
*/
List<ReservationOutMaterialDetailPO> queryOutStockNoticePrintDetail(ReservationStockOutOrderDO stockOutOrderDO);
Long queryShipperId(@Param("userMemberCode") String userMemberCode,@Param("organizationId") Long organizationId);
/**
* 按客户编号+组织查询客户用户表返回客户ID/编号/名称
* 客户编号兼容两种编码USER.USER_MEMBER_CODE客户编码 USER_SHIPPER.CUSTOMER_NC_CODENC客户代码/khdm任一命中即可
* 用于出入库导入时校验客户是否存在并自动匹配客户名称结果为空表示库中无该客户
*/
UserPo queryCustomerByCode(@Param("userMemberCode") String userMemberCode, @Param("organizationId") Long organizationId);
int countByOrderNumberPrefix(@Param("prefix") String prefix);