diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/businessDocumentOrder/repository/mapper/BusinessDocumentOrderMapper.java b/mhd_oms/src/main/java/com/mhd/oms/domain/businessDocumentOrder/repository/mapper/BusinessDocumentOrderMapper.java index 3d11a02d8..67530624e 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/domain/businessDocumentOrder/repository/mapper/BusinessDocumentOrderMapper.java +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/businessDocumentOrder/repository/mapper/BusinessDocumentOrderMapper.java @@ -39,8 +39,8 @@ public interface BusinessDocumentOrderMapper extends BaseMapper selectGoodsNumbersByPrefix(@Param("prefix") String prefix); } diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/businessDocumentOrder/repository/persistence/BusinessDocumentOrderImpl.java b/mhd_oms/src/main/java/com/mhd/oms/domain/businessDocumentOrder/repository/persistence/BusinessDocumentOrderImpl.java index 4b5f43efa..1ab5894ba 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/domain/businessDocumentOrder/repository/persistence/BusinessDocumentOrderImpl.java +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/businessDocumentOrder/repository/persistence/BusinessDocumentOrderImpl.java @@ -237,11 +237,8 @@ public class BusinessDocumentOrderImpl extends ServiceImpl() - .eq(ReservationStockOutOrder::getOutOrderNumber, tmsOrder.getInOrderNumber()) - .eq(ReservationStockOutOrder::getDelFlag, 1)); - if (reservationStockOutOrder != null){ - reservationStockOutOrder.setBusinessNumber(tmsOrder.getGoodsNumber()); - reservationStockOutOrderMapper.updateById(reservationStockOutOrder); - } - ReservationStockInOrder reservationStockInOrder = reservationStockInOrderMapper.selectOne(new LambdaQueryWrapper() - .eq(ReservationStockInOrder::getInOrderNumber, tmsOrder.getInOrderNumber()) - .eq(ReservationStockInOrder::getDelFlag, 1)); - if (reservationStockInOrder != null){ - reservationStockInOrder.setBusinessNumber(tmsOrder.getGoodsNumber()); - reservationStockInOrderMapper.updateById(reservationStockInOrder); - } + //生成货源单号并落库见下方 insertWithUniqueGoodsNumber: + //取号与插单合并为原子重试(撞goods_number唯一索引自动重新取号), + //出库/入库业务单的单号回填也移至插单成功之后,避免插单失败留下脏回填 List loadingAddressList = businessDocumentOrderDO.getLoadingAddressList();//装货地信息集合 List unloadAddressList = businessDocumentOrderDO.getUnloadAddressList();//卸货地信息集合 @@ -426,7 +411,24 @@ public class BusinessDocumentOrderDomainService { //一天后 tmsOrder.setDocumentDate(businessDocumentOrderDO.getDocumentDate()); - boolean istrue = businessDocumentOrderMapper.insert(tmsOrder) > 0; + //生成货源单号(yyMMdd + 当日顺序号,每日重置)并落库, 撞唯一索引自动重新取号重试 + boolean istrue = goodsNumberGenerator.insertWithUniqueGoodsNumber(tmsOrder); + + //落库成功后将运输业务单号回填至关联的出库/入库业务单 + ReservationStockOutOrder reservationStockOutOrder = reservationStockOutOrderMapper.selectOne(new LambdaQueryWrapper() + .eq(ReservationStockOutOrder::getOutOrderNumber, tmsOrder.getInOrderNumber()) + .eq(ReservationStockOutOrder::getDelFlag, 1)); + if (reservationStockOutOrder != null){ + reservationStockOutOrder.setBusinessNumber(tmsOrder.getGoodsNumber()); + reservationStockOutOrderMapper.updateById(reservationStockOutOrder); + } + ReservationStockInOrder reservationStockInOrder = reservationStockInOrderMapper.selectOne(new LambdaQueryWrapper() + .eq(ReservationStockInOrder::getInOrderNumber, tmsOrder.getInOrderNumber()) + .eq(ReservationStockInOrder::getDelFlag, 1)); + if (reservationStockInOrder != null){ + reservationStockInOrder.setBusinessNumber(tmsOrder.getGoodsNumber()); + reservationStockInOrderMapper.updateById(reservationStockInOrder); + } if (hasDetails) { for (BusinessDeliveryDetailsLinkDTO businessDeliveryDetailsLinkDTO : businessDeliveryDetailsLinkDTOList) { diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/businessDocumentOrder/service/GoodsNumberGenerator.java b/mhd_oms/src/main/java/com/mhd/oms/domain/businessDocumentOrder/service/GoodsNumberGenerator.java index 285c8a89f..c78b58bbd 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/domain/businessDocumentOrder/service/GoodsNumberGenerator.java +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/businessDocumentOrder/service/GoodsNumberGenerator.java @@ -5,23 +5,44 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.mhd.common.core.exception.ServiceException; import com.mhd.oms.domain.businessDocumentOrder.entity.BusinessDocumentOrder; import com.mhd.oms.domain.businessDocumentOrder.repository.mapper.BusinessDocumentOrderMapper; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import java.util.Date; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; /** * 业务单据订单(goodsNumber)单号生成器 * 规则: yyMMdd(取自单据日期) + 当日顺序号(每日重置, 3位起步, 超999自动扩位) * 示例: 260804001、260804999、2608041000 + * + * 唯一性保障(不依赖唯一索引, 三层防线): + * 1) 取号: 当日已生成单号 MAX+1 作为基线 + 候选号存在性校验后移收敛 + * (对齐入库单号修复先例 ReservationStockInOrderDomainService#generateReserveOrderNumber) + * 2) 落库: 按日期前缀分段的JVM锁串行化"取号+插单", 消除本实例内并发审核/ + * 批量取号的check-then-act竞态(多实例部署时本层只覆盖各自实例) + * 3) 自检自愈: 插单后立即校验同号数量, >1 说明与并发插入撞号, 给本单重新 + * 取号改号, 兜住跨实例残余竞态 + * 若日后补建goods_number唯一索引(见 sql/20260906_oms_business_goods_number_unique.sql), + * 撞号将抛DuplicateKeyException由重试逻辑接住, 与上述防线叠加互不冲突 */ +@Slf4j @Component public class GoodsNumberGenerator { private static final int MAX_RETRY = 100; + /** + * 取号+插单分段锁: 按单号日期前缀(yyMMdd)分段, 同日串行、跨日并行, + * 锁对象常驻(每日新增1个, 增量可忽略) + */ + private static final Map LOCK_STRIPES = new ConcurrentHashMap<>(); + @Autowired private BusinessDocumentOrderMapper businessDocumentOrderMapper; @@ -33,8 +54,21 @@ public class GoodsNumberGenerator { Date date = (documentDate == null) ? new Date() : documentDate; String datePart = DateUtil.format(date, "yyMMdd"); - // count 只查一次作为序号基线 - long seq = (long) businessDocumentOrderMapper.countByGoodsNumberPrefix(datePart) + 1; + // 当日已生成单号的最大序号作为基线(MAX+1而非COUNT+1, 防脏数据/序号错位导致回退复用) + List existNumbers = businessDocumentOrderMapper.selectGoodsNumbersByPrefix(datePart); + long seq = 0; + for (String existNumber : existNumbers) { + if (existNumber == null || existNumber.length() <= datePart.length()) { + continue; + } + String numStr = existNumber.substring(datePart.length()); + if (!numStr.matches("\\d+")) { + continue; // 防脏数据(非纯数字后缀)导致解析失败 + } + seq = Math.max(seq, Long.parseLong(numStr)); + } + seq = seq + 1; + int retryCount = 0; while (retryCount < MAX_RETRY) { String orderCode = datePart + String.format("%03d", seq); @@ -46,8 +80,60 @@ public class GoodsNumberGenerator { if (CollectionUtils.isEmpty(exist)) { return orderCode; } - seq++; // ← 关键:查重失败,序号往后递增找空位,而不是重新count + seq++; // ← 关键:查重失败,序号往后递增找空位,而不是重新取基线 } throw new ServiceException("生成唯一业务单号失败,已达到最大重试次数:" + MAX_RETRY); } -} \ No newline at end of file + + /** + * 生成唯一业务单号并落库: JVM分段锁内完成"取号+插单", + * 插入撞goods_number唯一索引时自动重新取号重试(索引为可选兜底), + * 插单成功后自检同号并自愈改号。 + * 供业务单创建入口(手动/审核新增、审核拆单、预约确认)统一使用。 + * @return 是否插入成功(mapper.insert结果) + */ + public boolean insertWithUniqueGoodsNumber(BusinessDocumentOrder order) { + Date date = (order.getDocumentDate() == null) ? new Date() : order.getDocumentDate(); + String lockKey = DateUtil.format(date, "yyMMdd"); + synchronized (LOCK_STRIPES.computeIfAbsent(lockKey, k -> new Object())) { + int retryCount = 0; + while (retryCount < MAX_RETRY) { + retryCount++; + order.setGoodsNumber(generateUniqueGoodsNumber(order.getDocumentDate())); + try { + boolean inserted = businessDocumentOrderMapper.insert(order) > 0; + // 插单后自检自愈: 无唯一索引时跨实例并发窗口内的撞号双方都能落库, + // 同号数量>1说明本单撞号, 给本单重新取号改号, 防止单号重复扩散 + healIfDuplicated(order); + return inserted; + } catch (DuplicateKeyException e) { + // 已建唯一索引时的兜底: 仅业务单号撞唯一索引时重试, 其他唯一约束冲突原样抛出 + String message = e.getMessage(); + if (message == null || !message.toLowerCase().contains("goods_number")) { + throw e; + } + log.warn("业务单号[{}]撞号, 自动重新取号重试(第{}次): {}", order.getGoodsNumber(), retryCount, message); + } + } + throw new ServiceException("生成唯一业务单号失败,已达到最大重试次数:" + MAX_RETRY); + } + } + + /** + * 插单后自检自愈: 查同号数量, >1 说明有并发插入撞号(本单为后插入方), + * 保持先落库单据单号不变, 重新取号给本单改号 + */ + private void healIfDuplicated(BusinessDocumentOrder order) { + List sameNumber = businessDocumentOrderMapper.selectList( + new QueryWrapper().eq("goods_number", order.getGoodsNumber()) + ); + if (sameNumber != null && sameNumber.size() > 1) { + String oldNumber = order.getGoodsNumber(); + String newNumber = generateUniqueGoodsNumber(order.getDocumentDate()); + order.setGoodsNumber(newNumber); + businessDocumentOrderMapper.updateById(order); + log.warn("业务单号[{}]与并发插入撞号(无唯一索引兜底), 已自愈改号为[{}], orderId={}", + oldNumber, newNumber, order.getId()); + } + } +} diff --git a/mhd_oms/src/main/resources/mapper/businessDocumentOrder/BusinessDocumentOrderMapper.xml b/mhd_oms/src/main/resources/mapper/businessDocumentOrder/BusinessDocumentOrderMapper.xml index 7c9037567..e098fac0b 100644 --- a/mhd_oms/src/main/resources/mapper/businessDocumentOrder/BusinessDocumentOrderMapper.xml +++ b/mhd_oms/src/main/resources/mapper/businessDocumentOrder/BusinessDocumentOrderMapper.xml @@ -1278,8 +1278,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" select PUSH_VERIFY from NGWL_TEST_PRODUCT.SYS_ORGANIZATION where ORGANIZATION_ID = #{organizationId} - + SELECT goods_number FROM business_document_order WHERE goods_number LIKE concat(#{prefix}, '%') AND LENGTH(goods_number) <= 12