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:
+21
-1
@@ -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);
|
||||
|
||||
+2
@@ -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);
|
||||
}
|
||||
+52
-67
@@ -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();
|
||||
}
|
||||
}
|
||||
+24
-49
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+4
@@ -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>
|
||||
Reference in New Issue
Block a user