diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/executionStockInOrder/service/ExecutionStockInOrderDomainService.java b/mhd_oms/src/main/java/com/mhd/oms/domain/executionStockInOrder/service/ExecutionStockInOrderDomainService.java index b7e5b8fab..b28feee99 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/domain/executionStockInOrder/service/ExecutionStockInOrderDomainService.java +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/executionStockInOrder/service/ExecutionStockInOrderDomainService.java @@ -596,7 +596,10 @@ public class ExecutionStockInOrderDomainService { stockInOrder.setTopOrganizationId(topOrgId); wmsServiceFeign.omsInOrderAdd(stockInOrder); String inOrderNumber = stockInOrder.getInOrderNumber(); - List materialDetailList = materialDetailService.list(new QueryWrapper().lambda().eq(ExecutionInMaterialDetail::getInOrderNumber, inOrderNumber)); + // 只推有效明细(del_flag=1),软删旧行不推WMS,防止WMS侧计划数量虚高 + List materialDetailList = materialDetailService.list(new QueryWrapper().lambda() + .eq(ExecutionInMaterialDetail::getInOrderNumber, inOrderNumber) + .eq(ExecutionInMaterialDetail::getDelFlag, 1)); List 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().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().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); diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/repository/mapper/ReservationStockInOrderMapper.java b/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/repository/mapper/ReservationStockInOrderMapper.java index d5b289c17..8f6078a99 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/repository/mapper/ReservationStockInOrderMapper.java +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/repository/mapper/ReservationStockInOrderMapper.java @@ -75,4 +75,6 @@ public interface ReservationStockInOrderMapper extends BaseMapper generateReserveOrderNumber1(@Param("prefix") String prefix); + + int existsByOrderNumber(@Param("inOrderNumber") String inOrderNumber); } \ No newline at end of file diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/service/ReservationStockInOrderDomainService.java b/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/service/ReservationStockInOrderDomainService.java index 6623dfbbc..586e10ba5 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/service/ReservationStockInOrderDomainService.java +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/service/ReservationStockInOrderDomainService.java @@ -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 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 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 inOrderIds = stockInOrderDO.getInOrderIds(); + if (inOrderIds == null || inOrderIds.isEmpty()) { + throw new ServiceException("执行失败:未选择单据"); + } + List checkOrders = stockInOrderService.list( + new QueryWrapper().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 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 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 orderNumber1 = reservationStockInOrderMapper.generateReserveOrderNumber1(prefix); - List orderNumber2 = reserveStockInOrderMapper.generateReserveOrderNumber1(prefix); - List 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(); } } \ No newline at end of file diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/reserveStockInOrder/service/ReserveStockInOrderDomainService.java b/mhd_oms/src/main/java/com/mhd/oms/domain/reserveStockInOrder/service/ReserveStockInOrderDomainService.java index 8cbd6cb8e..0062525b3 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/domain/reserveStockInOrder/service/ReserveStockInOrderDomainService.java +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/reserveStockInOrder/service/ReserveStockInOrderDomainService.java @@ -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() + .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 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 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 list = materialDetailService.list(new LambdaQueryWrapper().eq(ReserveInMaterialDetail::getInOrderNumber, stockInOrderDO.getInOrderNumber())); + // 只回填有效明细的仓库信息,软删行(del_flag=2)不参与,防止下发复制时把软删行一并带走 + List list = materialDetailService.list(new LambdaQueryWrapper() + .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 materialDetailList = materialDetailService.list(new QueryWrapper().lambda().eq(ReserveInMaterialDetail::getInOrderNumber, oldInOrderNumber)); + // 单号生成已统一为跨表取号(见 ReservationStockInOrderDomainService#generateReserveOrderNumber), + // 预约单号与业务单号同池递增,下发沿用原单号不会与手动创建的业务单撞号,故不换号; + // 历史教训:此处曾尝试下发换号,但物料明细未同步换号导致"预约确认后物料明细查不到" + // 只复制有效明细(del_flag=1):编辑过的预约在明细表里留有软删旧行,不过滤会把旧行一起复制进业务单, + // 头表计划数量按本列表求和(见下方 quantity)会把软删行计入,导致业务单/执行单计划数量翻倍 + List materialDetailList = materialDetailService.list(new QueryWrapper().lambda() + .eq(ReserveInMaterialDetail::getInOrderNumber, oldInOrderNumber) + .eq(ReserveInMaterialDetail::getDelFlag, 1)); List 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(); } /** diff --git a/mhd_oms/src/main/resources/mapper/reservationStockInOrder/ReservationStockInOrderMapper.xml b/mhd_oms/src/main/resources/mapper/reservationStockInOrder/ReservationStockInOrderMapper.xml index e1e55aa0f..c4eea5ae8 100644 --- a/mhd_oms/src/main/resources/mapper/reservationStockInOrder/ReservationStockInOrderMapper.xml +++ b/mhd_oms/src/main/resources/mapper/reservationStockInOrder/ReservationStockInOrderMapper.xml @@ -676,4 +676,8 @@ FROM reservation_stock_in_order WHERE in_order_number LIKE concat(#{prefix}, '%') + + \ No newline at end of file