refactor(erp): 重构ERP单据单号生成逻辑以支持冲突重试
- 在 ErpNoRedisDAO 中新增带冲突重试机制的单号生成方法
- 优化付款、收款、发票及采销订单等单据的单号生成逻辑
- 移除各业务服务中冗余的单号唯一性手动校验与异常抛出代码
- 支持在 Redis 计数丢失导致单号冲突时自动递增重试获取新单号
- 设置单号生成最大重试次数为一万次以防止死循环
- 在 SRM 模块初始化 SQL 中新增 BPM 流程实例编号字段
| | |
| | | |
| | | import java.time.Duration; |
| | | import java.time.LocalDateTime; |
| | | import java.util.function.Predicate; |
| | | |
| | | |
| | | /** |
| | |
| | | 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; |
| | | |
| | | } |
| | |
| | | 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 |
| | |
| | | } |
| | | |
| | | // 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() |
| | |
| | | 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 |
| | |
| | | // 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); |
| | |
| | | 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 |
| | |
| | | } |
| | | |
| | | // 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) |
| | |
| | | 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()); |
| | |
| | | } |
| | | |
| | | // 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. 申请人 / 申请部门:取当前登录用户及其部门 |
| | |
| | | } |
| | | |
| | | // 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(); |
| | |
| | | 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 |
| | |
| | | } |
| | | |
| | | // 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)”状态 |
| | |
| | | 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 '更新者', |