fix(oms):入库业务单单号重复与入库链路计划数量翻倍修复

一、单号重复:业务单与客户预约两套取号计数器互不可见致撞号。
取号统一为跨两表MAX+1+存在性校验,保存前校验预取号占用,下发不再换号,Mapper新增existsByOrderNumber。
二、计划数量翻倍:编辑明细为软删(del_flag=2留存),下发查明细未过滤del_flag,
软删行被复制进下游单据并计入头表数量(执行单数量≈业务单×2+修改差值)。
1. 预约→业务单下发、执行单推WMS:明细查询补del_flag=1过滤
2. executeAndIssue(执行并下发):补防重复下发校验(issueTime非空拒绝)
3. pushInOrder/pushInOrderDetail:加幂等,防Feign重试插重复单
This commit is contained in:
rcx
2026-08-20 14:06:50 +08:00
parent ef13d890be
commit ff2b1965f7
5 changed files with 103 additions and 117 deletions
@@ -596,7 +596,10 @@ public class ExecutionStockInOrderDomainService {
stockInOrder.setTopOrganizationId(topOrgId);
wmsServiceFeign.omsInOrderAdd(stockInOrder);
String inOrderNumber = stockInOrder.getInOrderNumber();
List<ExecutionInMaterialDetail> materialDetailList = materialDetailService.list(new QueryWrapper<ExecutionInMaterialDetail>().lambda().eq(ExecutionInMaterialDetail::getInOrderNumber, inOrderNumber));
// 只推有效明细(del_flag=1),软删旧行不推WMS,防止WMS侧计划数量虚高
List<ExecutionInMaterialDetail> materialDetailList = materialDetailService.list(new QueryWrapper<ExecutionInMaterialDetail>().lambda()
.eq(ExecutionInMaterialDetail::getInOrderNumber, inOrderNumber)
.eq(ExecutionInMaterialDetail::getDelFlag, 1));
List<InMaterialDetail> inMaterialDetailList = new ArrayList<>();
for (ExecutionInMaterialDetail reservationInMaterialDetail : materialDetailList) {
InMaterialDetail inMaterialDetail = new InMaterialDetail();
@@ -688,6 +691,13 @@ public class ExecutionStockInOrderDomainService {
}
public Boolean pushInOrder(StockInOrderTZPD stockInOrder) {
// 幂等防御:WMS回推(盘盈入库/库存调整)走Feign,超时重试会重放同一请求体(单号相同),
// 同单号已存在则跳过插入,防止重复执行单;正常业务每次推送都生成新单号,不受影响
long exists = stockInOrderService.count(new QueryWrapper<ExecutionStockInOrder>().lambda()
.eq(ExecutionStockInOrder::getInOrderNumber, stockInOrder.getInOrderNumber()));
if (exists > 0) {
return true;
}
ExecutionStockInOrder reservationStockInOrder = new ExecutionStockInOrder();
BeanUtils.copyProperties(stockInOrder, reservationStockInOrder);
reservationStockInOrder.setIssueStatus(4);
@@ -695,6 +705,16 @@ public class ExecutionStockInOrderDomainService {
}
public Boolean pushInOrderDetail(InMaterialDetailTZPD inMaterialDetail) {
// 幂等防御:同一请求体重放时单号+来源唯一ID均相同,已存在则跳过;
// uniqueId为空(调用方未传)时无从判重,保持原插入行为
if (inMaterialDetail.getUniqueId() != null) {
long exists = materialDetailService.count(new QueryWrapper<ExecutionInMaterialDetail>().lambda()
.eq(ExecutionInMaterialDetail::getInOrderNumber, inMaterialDetail.getInOrderNumber())
.eq(ExecutionInMaterialDetail::getUniqueId, inMaterialDetail.getUniqueId()));
if (exists > 0) {
return true;
}
}
ExecutionInMaterialDetail reservationInMaterialDetail = new ExecutionInMaterialDetail();
BeanUtils.copyProperties(inMaterialDetail, reservationInMaterialDetail);
return materialDetailService.save(reservationInMaterialDetail);
@@ -75,4 +75,6 @@ public interface ReservationStockInOrderMapper extends BaseMapper<ReservationSto
int countByOrderNumberPrefix(@Param("prefix") String prefix);
List<String> generateReserveOrderNumber1(@Param("prefix") String prefix);
int existsByOrderNumber(@Param("inOrderNumber") String inOrderNumber);
}
@@ -147,51 +147,17 @@ public class ReservationStockInOrderDomainService {
*/
@Transactional(rollbackFor = Exception.class)
public Boolean insert(ReservationStockInOrderDO stockInOrderDO) {
// 前端预取号可能已被他人先保存占用,占用则废弃重新取号
if (stockInOrderDO.getInOrderNumber() != null
&& (reservationStockInOrderMapper.existsByOrderNumber(stockInOrderDO.getInOrderNumber()) > 0
|| reserveStockInOrderMapper.countByInOrderNumber(stockInOrderDO.getInOrderNumber()) > 0)) {
stockInOrderDO.setInOrderNumber(null);
}
if (stockInOrderDO.getInOrderNumber() == null) {
//设置入库单号
// Long warehouseId = stockInOrderDO.getWarehouseId();
// AjaxResult ajaxResult = systemServiceFeign.getWarehouseInfoByWarehouseId(warehouseId);
// if (!"200".equals(String.valueOf(ajaxResult.get("code")))) {
// throw new ServiceException("获取仓库信息失败");
// }
// //仓库信息
// WarehouseFeignPO warehouseFeignPO = com.alibaba.fastjson.JSON.parseObject(com.alibaba.fastjson2.JSONObject.toJSONString(ajaxResult.get("data")), WarehouseFeignPO.class);
// String orderNoGenerateRule = warehouseFeignPO.getOrderNoGenerateRule();
// String prefix = warehouseFeignPO.getInOrderNoPrefix();
// if (prefix == null || prefix.isEmpty()) {
// prefix = "RK";
// }
// Long organizationId = stockInOrderDO.getOrganizationId();
// Long warehouseIdForCount = stockInOrderDO.getWarehouseId();
// if (orderNoGenerateRule != null && "0".equals(orderNoGenerateRule)) {
// //日期累加: 前缀 + 当前日期yymmdd + 当天最大序号+1
// String dateStr = new SimpleDateFormat("yyMMdd").format(new Date());
// String datePrefix = prefix + dateStr;
// List<String> orderNumbers = reserveStockInOrderMapper.getMaxStockInOrderByPrefixAndOrgAndWarehouse(datePrefix, organizationId, warehouseIdForCount);
// int maxSeq = extractMaxSequenceNumber(orderNumbers, datePrefix.length());
// String seq = String.format("%03d", maxSeq + 1);
// stockInOrderDO.setInOrderNumber(datePrefix + seq);
// stockInOrderDO.setBusinessOrderNo(datePrefix + seq);
// } else {
// //历史累加: 前缀 + 历史最大序号+1
// List<String> orderNumbers = reserveStockInOrderMapper.getMaxStockInOrderByPrefixAndOrgAndWarehouse(prefix, organizationId, warehouseIdForCount);
// int maxSeq = extractMaxSequenceNumber(orderNumbers, prefix.length());
// String seq = String.format("%03d", maxSeq + 1);
// stockInOrderDO.setInOrderNumber(prefix + seq);
// stockInOrderDO.setBusinessOrderNo(prefix + seq);
// }
stockInOrderDO.setInOrderNumber(generateReserveOrderNumber());
stockInOrderDO.setBusinessOrderNo(stockInOrderDO.getInOrderNumber());
}
// //设置默认值
// setDefaultValues(stockInOrderDO);
// //设置物料信息
// setMaterialDetailInfo(stockInOrderDO);
// //统计总物料种数
// stockInOrderDO.setMaterialQuantity(stockInOrderDO.getMaterialDetailList().size());
// //统计总计划数
//统计总计划数
BigDecimal quantity = stockInOrderDO.getMaterialDetailList().stream().map(ReservationInMaterialDetailDO::getQuantity).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
stockInOrderDO.setQuantity(quantity);
@@ -794,6 +760,21 @@ public class ReservationStockInOrderDomainService {
@Transactional(rollbackFor = Exception.class)
public Boolean executeAndIssue(ReservationStockInOrderDO stockInOrderDO) {
// 防重复执行校验(与 issueOrder 的防重逻辑对齐):issueTime 有值表示已下发/执行过。
// 此前缺少该校验,重复调用会重复生成执行单,导致执行单计划数量合计≈业务单的2倍
List<Long> inOrderIds = stockInOrderDO.getInOrderIds();
if (inOrderIds == null || inOrderIds.isEmpty()) {
throw new ServiceException("执行失败:未选择单据");
}
List<ReservationStockInOrder> checkOrders = stockInOrderService.list(
new QueryWrapper<ReservationStockInOrder>().lambda()
.in(ReservationStockInOrder::getInOrderId, inOrderIds));
for (ReservationStockInOrder order : checkOrders) {
if (order.getIssueTime() != null) {
throw new ServiceException("执行失败:单据[" + order.getInOrderNumber() + "]已下发,不可重复执行;如需重新下发请先取消下发");
}
}
LoginUser loginUser = SecurityUtils.getLoginUser();
// 获取用户姓名(优先从UserPo获取,如果为空则使用realname,最后使用username作为兜底)
@@ -830,7 +811,6 @@ public class ReservationStockInOrderDomainService {
// wmsServiceFeign.omsInOrderAddDetail(inMaterialDetailList);
// }
//todo下发推送到入库业务单
List<Long> inOrderIds = stockInOrderDO.getInOrderIds();
for (Long inOrderId : inOrderIds) {
// Long inOrderId = stockInOrderDO.getInOrderId();
ReservationStockInOrder byId = stockInOrderService.getById(inOrderId);
@@ -1453,36 +1433,41 @@ public class ReservationStockInOrderDomainService {
}
/**
* 生成入库预约单号:
* 生成入库单号(R+yyMMdd+序号):
* 跨 reserve_stock_in_order / reservation_stock_in_order 两表取当日最大序号+1,
* 并对候选号做存在性校验,防止:
* 1) 手动创建与客户预约两套计数器互不可见导致撞号
* 2) COUNT+1 在删单/作废后序号回退复用
*/
public String generateReserveOrderNumber() {
SimpleDateFormat sdf = new SimpleDateFormat("yyMMdd");
String datePart = sdf.format(new Date());
String prefix = "R" + datePart;
int todayCount = reservationStockInOrderMapper.countByOrderNumberPrefix(prefix);
String seq = String.format("%03d", todayCount + 1);
return prefix + seq;
String prefix = "R" + sdf.format(new Date());
while (true) {
List<String> orderNumbers = new ArrayList<>();
orderNumbers.addAll(reservationStockInOrderMapper.generateReserveOrderNumber1(prefix));
orderNumbers.addAll(reserveStockInOrderMapper.generateReserveOrderNumber1(prefix));
int maxSeq = 0;
for (String order : orderNumbers) {
if (order == null || order.length() <= prefix.length()) {
continue;
}
String numStr = order.substring(prefix.length());
if (!numStr.matches("\\d+")) {
continue; // 防脏数据(非纯数字后缀)导致解析失败
}
maxSeq = Math.max(maxSeq, Integer.parseInt(numStr));
}
String candidate = prefix + String.format("%03d", maxSeq + 1);
// 候选号已被占用(并发插入窗口)则重试,下一轮 MAX 会包含该号,循环必然收敛
if (reservationStockInOrderMapper.existsByOrderNumber(candidate) == 0
&& reserveStockInOrderMapper.countByInOrderNumber(candidate) == 0) {
return candidate;
}
}
}
public String generateReserveOrderNumber1() {
SimpleDateFormat sdf = new SimpleDateFormat("yyMMdd");
String datePart = sdf.format(new Date());
String prefix = "R" + datePart;
List<String> orderNumber1 = reservationStockInOrderMapper.generateReserveOrderNumber1(prefix);
List<String> orderNumber2 = reserveStockInOrderMapper.generateReserveOrderNumber1(prefix);
List<String> allOrders = new ArrayList<>();
allOrders.addAll(orderNumber1);
allOrders.addAll(orderNumber2);
int maxseq = 0;
for(String order:allOrders)
{
String numStr = order.substring(prefix.length());
int num = Integer.parseInt(numStr);
if(num > maxseq){
maxseq = num;
}
}
String seq = String.format("%03d", maxseq + 1);
return prefix + seq;
// 与 generateReserveOrderNumber 收敛为同一套跨表取号逻辑,避免两份实现日后分叉
return generateReserveOrderNumber();
}
}
@@ -103,45 +103,19 @@ public class ReserveStockInOrderDomainService {
*/
@Transactional(rollbackFor = Exception.class)
public Boolean insert(ReserveStockInOrderDO stockInOrderDO) {
// 前端预取号可能已被他人先保存占用,占用则废弃重新取号
// :本类字段 reservationStockInOrderMapper 实为 ReserveStockInOrderMapper(查预约表),
// 业务单表(reservation_stock_in_order) reservationStockInOrderService 查询
if (stockInOrderDO.getInOrderNumber() != null
&& (reservationStockInOrderMapper.countByInOrderNumber(stockInOrderDO.getInOrderNumber()) > 0
|| reservationStockInOrderService.count(new LambdaQueryWrapper<ReservationStockInOrder>()
.eq(ReservationStockInOrder::getInOrderNumber, stockInOrderDO.getInOrderNumber())) > 0)) {
stockInOrderDO.setInOrderNumber(null);
}
if (stockInOrderDO.getInOrderNumber() == null) {
stockInOrderDO.setInOrderNumber(generateReserveOrderNumber());
// Long warehouseId = stockInOrderDO.getWarehouseId();
// AjaxResult ajaxResult = systemServiceFeign.getWarehouseInfoByWarehouseId(warehouseId);
// if (!"200".equals(String.valueOf(ajaxResult.get("code")))) {
// throw new ServiceException("获取仓库信息失败");
// }
// //仓库信息
// WarehouseFeignPO warehouseFeignPO = com.alibaba.fastjson.JSON.parseObject(com.alibaba.fastjson2.JSONObject.toJSONString(ajaxResult.get("data")), WarehouseFeignPO.class);
// String orderNoGenerateRule = warehouseFeignPO.getOrderNoGenerateRule();
// String prefix = warehouseFeignPO.getInOrderNoPrefix();
// if (prefix == null || prefix.isEmpty()) {
// prefix = "RK";
// }
// Long organizationId = stockInOrderDO.getOrganizationId();
// Long warehouseIdForCount = stockInOrderDO.getWarehouseId();
// if (orderNoGenerateRule != null && "0".equals(orderNoGenerateRule)) {
// //日期累加: 前缀 + 当前日期yymmdd + 当天最大序号+1
// String dateStr = new SimpleDateFormat("yyMMdd").format(new Date());
// String datePrefix = prefix + dateStr;
// List<String> orderNumbers = reservationStockInOrderMapper.getMaxStockInOrderByPrefixAndOrgAndWarehouse(datePrefix, organizationId, warehouseIdForCount);
// int maxSeq = extractMaxSequenceNumber(orderNumbers, datePrefix.length());
// String seq = String.format("%03d", maxSeq + 1);
// stockInOrderDO.setInOrderNumber(datePrefix + seq);
// stockInOrderDO.setBusinessOrderNo(datePrefix + seq);
// } else {
// //历史累加: 前缀 + 历史最大序号+1
// List<String> orderNumbers = reservationStockInOrderMapper.getMaxStockInOrderByPrefixAndOrgAndWarehouse(prefix, organizationId, warehouseIdForCount);
// int maxSeq = extractMaxSequenceNumber(orderNumbers, prefix.length());
// String seq = String.format("%03d", maxSeq + 1);
// stockInOrderDO.setInOrderNumber(prefix + seq);
// stockInOrderDO.setBusinessOrderNo(prefix + seq);
// }
stockInOrderDO.setBusinessOrderNo(stockInOrderDO.getInOrderNumber());
}
// //设置默认值
// setDefaultValues(stockInOrderDO);
// //设置物料信息
// setMaterialDetailInfo(stockInOrderDO);
// //统计总物料种数
if (CollectionUtils.isEmpty(stockInOrderDO.getMaterialDetailList())) {
throw new ServiceException("请添加货物明细");
}
@@ -556,7 +530,10 @@ public class ReserveStockInOrderDomainService {
.set(ReserveStockInOrder::getWarehouseName, stockInOrderDO.getWarehouseName())
.eq(ReserveStockInOrder::getInOrderId, stockInOrderDO.getInOrderId()));
List<ReserveInMaterialDetail> list = materialDetailService.list(new LambdaQueryWrapper<ReserveInMaterialDetail>().eq(ReserveInMaterialDetail::getInOrderNumber, stockInOrderDO.getInOrderNumber()));
// 只回填有效明细的仓库信息软删行(del_flag=2)不参与防止下发复制时把软删行一并带走
List<ReserveInMaterialDetail> list = materialDetailService.list(new LambdaQueryWrapper<ReserveInMaterialDetail>()
.eq(ReserveInMaterialDetail::getInOrderNumber, stockInOrderDO.getInOrderNumber())
.eq(ReserveInMaterialDetail::getDelFlag, 1));
for (ReserveInMaterialDetail reserveInMaterialDetail : list) {
reserveInMaterialDetail.setWarehouseId(stockInOrderDO.getWarehouseId());
reserveInMaterialDetail.setWarehouseCode(stockInOrderDO.getWarehouseCode());
@@ -578,17 +555,19 @@ public class ReserveStockInOrderDomainService {
String jsbz = reservationStockInOrderMapper.getJsbz(shipperId);
reservationStockInOrder.setSettlementCurrency(jsbz);
String oldInOrderNumber = reservationStockInOrder.getInOrderNumber();
// 生成新业务单号防止与已生成的入库业务单号重复
String newOrderNumber = reservationStockInOrderDomainService.generateReserveOrderNumber();
// reservationStockInOrder.setInOrderNumber(newOrderNumber);
// reservationStockInOrder.setBusinessOrderNo(newOrderNumber);
List<ReserveInMaterialDetail> materialDetailList = materialDetailService.list(new QueryWrapper<ReserveInMaterialDetail>().lambda().eq(ReserveInMaterialDetail::getInOrderNumber, oldInOrderNumber));
// 单号生成已统一为跨表取号( ReservationStockInOrderDomainService#generateReserveOrderNumber),
// 预约单号与业务单号同池递增,下发沿用原单号不会与手动创建的业务单撞号,故不换号;
// 历史教训:此处曾尝试下发换号,但物料明细未同步换号导致"预约确认后物料明细查不到"
// 只复制有效明细(del_flag=1)编辑过的预约在明细表里留有软删旧行不过滤会把旧行一起复制进业务单
// 头表计划数量按本列表求和(见下方 quantity)会把软删行计入导致业务单/执行单计划数量翻倍
List<ReserveInMaterialDetail> materialDetailList = materialDetailService.list(new QueryWrapper<ReserveInMaterialDetail>().lambda()
.eq(ReserveInMaterialDetail::getInOrderNumber, oldInOrderNumber)
.eq(ReserveInMaterialDetail::getDelFlag, 1));
List<ReservationInMaterialDetail> inMaterialDetailList = new ArrayList<>();
int index = 1;
for (ReserveInMaterialDetail reserveInMaterialDetail : materialDetailList) {
ReservationInMaterialDetail reservationInMaterialDetail = new ReservationInMaterialDetail();
BeanUtils.copyProperties(reserveInMaterialDetail, reservationInMaterialDetail,"materialDetailId");
// reservationInMaterialDetail.setInOrderNumber(newOrderNumber); // 使用新单号保持与主表关联
reservationInMaterialDetail.setQuantity(reserveInMaterialDetail.getReserveQuantity());
String lot = generateDateSerialNumber();
lot = lot+"-"+index;
@@ -1088,12 +1067,8 @@ public class ReserveStockInOrderDomainService {
* 生成入库预约单号
*/
public String generateReserveOrderNumber() {
SimpleDateFormat sdf = new SimpleDateFormat("yyMMdd");
String datePart = sdf.format(new Date());
String prefix = "R" + datePart;
int todayCount = reservationStockInOrderMapper.countByOrderNumberPrefix(prefix);
String seq = String.format("%03d", todayCount + 1);
return prefix + seq;
// 统一走跨表取号,避免预约表与业务单表两套计数器撞号
return reservationStockInOrderDomainService.generateReserveOrderNumber();
}
/**
@@ -676,4 +676,8 @@
FROM reservation_stock_in_order
WHERE in_order_number LIKE concat(#{prefix}, '%')
</select>
<select id="existsByOrderNumber" resultType="java.lang.Integer" parameterType="java.lang.String">
SELECT COUNT(1) FROM reservation_stock_in_order WHERE in_order_number = #{inOrderNumber}
</select>
</mapper>