云
5 天以前 1d9cfc91e7255b9e139b20ff230cb19246f19193
refactor(erp): 重构ERP单据单号生成逻辑以支持冲突重试

- 在 ErpNoRedisDAO 中新增带冲突重试机制的单号生成方法
- 优化付款、收款、发票及采销订单等单据的单号生成逻辑
- 移除各业务服务中冗余的单号唯一性手动校验与异常抛出代码
- 支持在 Redis 计数丢失导致单号冲突时自动递增重试获取新单号
- 设置单号生成最大重试次数为一万次以防止死循环
- 在 SRM 模块初始化 SQL 中新增 BPM 流程实例编号字段
已修改8个文件
111 ■■■■■ 文件已修改
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinanceReceiptServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInvoiceServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/resources/sql/srm_init.sql 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java
@@ -9,6 +9,7 @@
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.function.Predicate;
/**
@@ -68,4 +69,26 @@
        return noPrefix + String.format("%06d", no);
    }
    /**
     * 生成序号,并跳过已存在的单号。
     *
     * Redis 中的自增计数一旦丢失(如 Redis 重启未持久化、手工清库),计数会从 1 重新开始,
     * 而当天数据库中可能已有同日期后缀的单号,此时逐个递增重试直到取到未占用的序号。
     *
     * @param prefix  前缀
     * @param isNoExists 单号是否已存在
     * @return 未占用的序号
     */
    public String generate(String prefix, Predicate<String> isNoExists) {
        for (int i = 0; i < MAX_GENERATE_ATTEMPTS; i++) {
            String no = generate(prefix);
            if (!isNoExists.test(no)) {
                return no;
            }
        }
        throw new IllegalStateException("生成单号失败:连续 " + MAX_GENERATE_ATTEMPTS + " 次尝试均已被占用,请稍后重试");
    }
    private static final int MAX_GENERATE_ATTEMPTS = 10000;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentServiceImpl.java
@@ -105,10 +105,8 @@
                getSumValue(paymentItems, ErpFinancePaymentItemDO::getPaymentPrice, BigDecimal::add, BigDecimal.ZERO)
                        .subtract(createReqVO.getDiscountPrice()), null);
        // 1.7 生成付款单号,并校验唯一性
        String no = noRedisDAO.generate(ErpNoRedisDAO.FINANCE_PAYMENT_NO_PREFIX);
        if (financePaymentMapper.selectByNo(no) != null) {
            throw exception(FINANCE_PAYMENT_NO_EXISTS);
        }
        String no = noRedisDAO.generate(ErpNoRedisDAO.FINANCE_PAYMENT_NO_PREFIX,
                candidate -> financePaymentMapper.selectByNo(candidate) != null);
        // 2.1 插入付款单
        ErpFinancePaymentDO payment = BeanUtils.toBean(createReqVO, ErpFinancePaymentDO.class, in -> in
@@ -433,10 +431,8 @@
        }
        // 2.1 生成付款单号
        String no = noRedisDAO.generate(ErpNoRedisDAO.FINANCE_PAYMENT_NO_PREFIX);
        if (financePaymentMapper.selectByNo(no) != null) {
            throw exception(FINANCE_PAYMENT_NO_EXISTS);
        }
        String no = noRedisDAO.generate(ErpNoRedisDAO.FINANCE_PAYMENT_NO_PREFIX,
                candidate -> financePaymentMapper.selectByNo(candidate) != null);
        // 2.2 金额按负数入账
        BigDecimal negativeAmount = reqDTO.getRefundAmount().negate();
        ErpFinancePaymentDO payment = new ErpFinancePaymentDO()
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinanceReceiptServiceImpl.java
@@ -73,10 +73,8 @@
            adminUserApi.validateUser(createReqVO.getFinanceUserId());
        }
        // 1.5 生成收款单号,并校验唯一性
        String no = noRedisDAO.generate(ErpNoRedisDAO.FINANCE_RECEIPT_NO_PREFIX);
        if (financeReceiptMapper.selectByNo(no) != null) {
            throw exception(FINANCE_RECEIPT_NO_EXISTS);
        }
        String no = noRedisDAO.generate(ErpNoRedisDAO.FINANCE_RECEIPT_NO_PREFIX,
                candidate -> financeReceiptMapper.selectByNo(candidate) != null);
        // 2.1 插入收款单
        ErpFinanceReceiptDO receipt = BeanUtils.toBean(createReqVO, ErpFinanceReceiptDO.class, in -> in
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInvoiceServiceImpl.java
@@ -87,10 +87,8 @@
        // 1.2 校验可开票金额超过上限
        validatePurchaseInvoicePriceExceedsLimit(createReqVO);
        // 1.3 生成来票编号
        String no = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_INVOICE_NO_PREFIX);
        if (invoiceMapper.selectByNo(no) != null) {
            throw exception(PURCHASE_INVOICE_NO_EXISTS);
        }
        String no = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_INVOICE_NO_PREFIX,
                candidate -> invoiceMapper.selectByNo(candidate) != null);
        // 2.1 插入来票(创建后即为有效状态,无需审核)
        ErpPurchaseInvoiceDO invoice = BeanUtils.toBean(createReqVO, ErpPurchaseInvoiceDO.class).setNo(no);
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java
@@ -136,10 +136,8 @@
            accountService.validateAccount(createReqVO.getAccountId());
        }
        // 1.4 生成订单号,并校验唯一性
        String no = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_ORDER_NO_PREFIX);
        if (purchaseOrderMapper.selectByNo(no) != null) {
            throw exception(PURCHASE_ORDER_NO_EXISTS);
        }
        String no = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_ORDER_NO_PREFIX,
                candidate -> purchaseOrderMapper.selectByNo(candidate) != null);
        // 2.1 插入订单(submit=false 时保存为草稿,等待业务确认后手动提交审批)
        ErpPurchaseOrderDO purchaseOrder = BeanUtils.toBean(createReqVO, ErpPurchaseOrderDO.class, in -> in
@@ -307,10 +305,15 @@
        }
        // 3. 单号:填了就用(校验唯一),没填自动生成
        String no = StrUtil.isNotBlank(head.getNo()) ? head.getNo().trim()
                : noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_ORDER_NO_PREFIX);
        if (purchaseOrderMapper.selectByNo(no) != null) {
            throw new IllegalArgumentException("采购单编号已存在:" + no);
        String no;
        if (StrUtil.isNotBlank(head.getNo())) {
            no = head.getNo().trim();
            if (purchaseOrderMapper.selectByNo(no) != null) {
                throw new IllegalArgumentException("采购单编号已存在:" + no);
            }
        } else {
            no = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_ORDER_NO_PREFIX,
                    candidate -> purchaseOrderMapper.selectByNo(candidate) != null);
        }
        // 4. 组装主表:勾选“是否审核” => 已审核(20),否则未审核(10)
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java
@@ -114,10 +114,8 @@
            supplierService.validateSupplier(createReqVO.getSupplierId());
        }
        // 3. 生成申请单号
        String no = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_REQUEST_NO_PREFIX);
        if (purchaseRequestMapper.selectByNo(no) != null) {
            throw exception(PURCHASE_REQUEST_NO_EXISTS);
        }
        String no = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_REQUEST_NO_PREFIX,
                candidate -> purchaseRequestMapper.selectByNo(candidate) != null);
        // 4. 获取当前用户信息
        Long userId = Long.parseLong(getCurrentUserId());
@@ -276,10 +274,15 @@
        }
        // 3. 单号:填了就用(校验唯一),没填自动生成 CGSQ...
        String no = StrUtil.isNotBlank(head.getNo()) ? head.getNo().trim()
                : noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_REQUEST_NO_PREFIX);
        if (purchaseRequestMapper.selectByNo(no) != null) {
            throw new IllegalArgumentException("采购申请单号已存在:" + no);
        String no;
        if (StrUtil.isNotBlank(head.getNo())) {
            no = head.getNo().trim();
            if (purchaseRequestMapper.selectByNo(no) != null) {
                throw new IllegalArgumentException("采购申请单号已存在:" + no);
            }
        } else {
            no = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_REQUEST_NO_PREFIX,
                    candidate -> purchaseRequestMapper.selectByNo(candidate) != null);
        }
        // 4. 申请人 / 申请部门:取当前登录用户及其部门
@@ -596,10 +599,8 @@
        }
        // 2. 生成采购订单号
        String orderNo = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_ORDER_NO_PREFIX);
        if (purchaseOrderMapper.selectByNo(orderNo) != null) {
            throw exception(PURCHASE_ORDER_NO_EXISTS);
        }
        String orderNo = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_ORDER_NO_PREFIX,
                candidate -> purchaseOrderMapper.selectByNo(candidate) != null);
        // 3. 创建采购订单
        ErpPurchaseOrderDO purchaseOrder = new ErpPurchaseOrderDO();
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java
@@ -139,10 +139,8 @@
            adminUserApi.validateUser(createReqVO.getSaleUserId());
        }
        // 1.5 生成订单号,并校验唯一性
        String no = noRedisDAO.generate(ErpNoRedisDAO.SALE_ORDER_NO_PREFIX);
        if (saleOrderMapper.selectByNo(no) != null) {
            throw exception(SALE_ORDER_NO_EXISTS);
        }
        String no = noRedisDAO.generate(ErpNoRedisDAO.SALE_ORDER_NO_PREFIX,
                candidate -> saleOrderMapper.selectByNo(candidate) != null);
        // 2.1 插入订单(创建后为未提交状态,需手动提交审批到协同办公)
        ErpSaleOrderDO saleOrder = BeanUtils.toBean(createReqVO, ErpSaleOrderDO.class, in -> in
@@ -306,10 +304,15 @@
        }
        // 3. 单号:填了就用(校验唯一),没填自动生成
        String no = StrUtil.isNotBlank(head.getNo()) ? head.getNo().trim()
                : noRedisDAO.generate(ErpNoRedisDAO.SALE_ORDER_NO_PREFIX);
        if (saleOrderMapper.selectByNo(no) != null) {
            throw new IllegalArgumentException("销售单编号已存在:" + no);
        String no;
        if (StrUtil.isNotBlank(head.getNo())) {
            no = head.getNo().trim();
            if (saleOrderMapper.selectByNo(no) != null) {
                throw new IllegalArgumentException("销售单编号已存在:" + no);
            }
        } else {
            no = noRedisDAO.generate(ErpNoRedisDAO.SALE_ORDER_NO_PREFIX,
                    candidate -> saleOrderMapper.selectByNo(candidate) != null);
        }
        // 4. 组装主表:导入后固定为“未提交(0)”状态
yudao-module-srm/src/main/resources/sql/srm_init.sql
@@ -108,6 +108,7 @@
    deadline DATE DEFAULT NULL COMMENT '报价截止日期',
    purchaser_user_id BIGINT DEFAULT NULL COMMENT '采购员用户ID',
    remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
    process_instance_id VARCHAR(64) DEFAULT NULL COMMENT 'BPM流程实例编号',
    creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updater VARCHAR(64) DEFAULT '' COMMENT '更新者',