From a647bc8a43c0212f482bee0630ebd14dca120a8a Mon Sep 17 00:00:00 2001
From: 吴羡 <16976746+Xian-TT3@user.noreply.gitee.com>
Date: 星期一, 28 九月 2026 14:13:28 +0800
Subject: [PATCH] feat: CRM 发票/合同导入、MES 质检批量导入与检测台账、回款与报价校验、权限码细化
---
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImpl.java | 250 ++++++++++++++++++++++++++++++++++++++++++++++++-
1 files changed, 243 insertions(+), 7 deletions(-)
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImpl.java
index 4d0fd42..a59c979 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImpl.java
+++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImpl.java
@@ -5,12 +5,16 @@
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
+import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
+import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoiceImportExcelVO;
+import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoiceImportRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoicePageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoiceSaveReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
@@ -36,11 +40,18 @@
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+import static cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_INVOICE_TYPE;
import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.crm.enums.LogRecordConstants.*;
import static cn.iocoder.yudao.module.crm.util.CrmAuditStatusUtils.convertBpmResultToAuditStatus;
@@ -118,22 +129,247 @@
return invoice.getId();
}
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public CrmInvoiceImportRespVO importInvoiceList(List<CrmInvoiceImportExcelVO> importList) {
+ CrmInvoiceImportRespVO respVO = CrmInvoiceImportRespVO.builder()
+ .createList(new ArrayList<>())
+ .failureList(new LinkedHashMap<>())
+ .build();
+ if (CollUtil.isEmpty(importList)) {
+ return respVO;
+ }
+
+ Map<String, Integer> firstRowNoByKey = new HashMap<>();
+ for (int i = 0; i < importList.size(); i++) {
+ CrmInvoiceImportExcelVO row = importList.get(i);
+ if (isImportRowEmpty(row)) {
+ continue; // 璺宠繃绌鸿
+ }
+ String label = StrUtil.isNotBlank(row.getInvoiceNo()) ? "鍙戠エ鍙风爜 " + row.getInvoiceNo().trim()
+ : "绗� " + (i + 2) + " 琛�";
+ String importKey = buildInvoiceImportKey(row);
+ try {
+ Integer firstRowNo = firstRowNoByKey.get(importKey);
+ if (firstRowNo != null) {
+ throw duplicateRowException(firstRowNo, buildInvoiceKeyDesc(row));
+ }
+ respVO.getCreateList().add(createInvoiceByImport(row));
+ firstRowNoByKey.putIfAbsent(importKey, i + 2);
+ } catch (Exception ex) {
+ respVO.getFailureList().put(label, resolveImportErrorMessage(ex));
+ }
+ }
+ return respVO;
+ }
+
+ private boolean isImportRowEmpty(CrmInvoiceImportExcelVO row) {
+ return row == null || (StrUtil.isBlank(row.getNo()) && StrUtil.isBlank(row.getContractNo())
+ && StrUtil.isBlank(row.getInvoiceType()) && StrUtil.isBlank(row.getInvoiceNo())
+ && StrUtil.isBlank(row.getInvoiceTitle()) && row.getPrice() == null
+ && StrUtil.isBlank(row.getInvoiceTime()) && StrUtil.isBlank(row.getRemark()));
+ }
+
+ /**
+ * 渚濇嵁瀵煎叆鏁版嵁鍒涘缓涓�寮犲紑绁ㄣ�傚鎴峰彇鍚堝悓鎵�灞炲鎴凤紱璐熻矗浜�=褰撳墠鐧诲綍鐢ㄦ埛锛涘鎵圭姸鎬�=鏈彁浜ゃ��
+ */
+ private String createInvoiceByImport(CrmInvoiceImportExcelVO row) {
+ // 1. 鍚堝悓鏍¢獙
+ if (StrUtil.isBlank(row.getContractNo())) {
+ throw new IllegalArgumentException("鍚堝悓缂栧彿涓嶈兘涓虹┖锛氳濉啓妯℃澘涓琛岀殑銆屽悎鍚岀紪鍙枫�嶅垪");
+ }
+ CrmContractDO contract = contractService.getContractByNo(row.getContractNo().trim());
+ if (contract == null) {
+ throw new IllegalArgumentException("鍚堝悓缂栧彿涓嶅瓨鍦細" + row.getContractNo().trim()
+ + "銆傝鍏堝湪銆孋RM 鍚堝悓绠$悊銆嶄腑鍒涘缓鎴栧鍏ヨ鍚堝悓锛堢紪鍙烽渶涓庢ā鏉垮畬鍏ㄤ竴鑷达級锛屾垨淇敼妯℃澘涓殑鍚堝悓缂栧彿鍚庨噸鏂板鍏�");
+ }
+ if (ObjUtil.notEqual(contract.getAuditStatus(), CrmAuditStatusEnum.APPROVE.getStatus())) {
+ throw new IllegalArgumentException("鍚堝悓[" + contract.getNo() + "]褰撳墠鐘舵�佷负銆�"
+ + CrmAuditStatusEnum.getNameByStatus(contract.getAuditStatus()) + "銆嶏紝鍙湁銆屽鏍搁�氳繃銆嶇殑鍚堝悓鎵嶈兘寮�绁ㄣ��"
+ + "璇峰厛鍦ㄣ�孋RM 鍚堝悓绠$悊銆嶄腑瀹屾垚璇ュ悎鍚屽鎵癸紝鎴栨敼鐢ㄥ叾浠栧凡瀹℃牳閫氳繃鐨勫悎鍚岀紪鍙峰悗閲嶆柊瀵煎叆");
+ }
+
+ // 2. 寮�绁ㄩ噾棰濇牎楠�
+ if (row.getPrice() == null) {
+ throw new IllegalArgumentException("寮�绁ㄩ噾棰濅笉鑳戒负绌猴細璇峰~鍐欐ā鏉夸腑璇ヨ鐨勩�屽紑绁ㄩ噾棰�(鍏�)銆嶅垪");
+ }
+ if (row.getPrice().compareTo(BigDecimal.ZERO) <= 0) {
+ throw new IllegalArgumentException("寮�绁ㄩ噾棰濆繀椤诲ぇ浜� 0锛屽綋鍓嶄负 "
+ + row.getPrice().stripTrailingZeros().toPlainString() + "銆傝淇敼妯℃澘涓殑銆屽紑绁ㄩ噾棰�(鍏�)銆嶅悗閲嶆柊瀵煎叆");
+ }
+ BigDecimal remainingInvoicePrice = getRemainingInvoicePrice(contract, null);
+ if (row.getPrice().compareTo(remainingInvoicePrice) > 0) {
+ throw new IllegalArgumentException("寮�绁ㄩ噾棰濊秴杩囧悎鍚屽墿浣欏彲寮�绁ㄩ噾棰濓細鍚堝悓[" + contract.getNo()
+ + "]鍓╀綑鍙紑绁ㄩ噾棰� " + remainingInvoicePrice.stripTrailingZeros().toPlainString() + " 鍏冿紝鏈瀵煎叆閲戦 "
+ + row.getPrice().stripTrailingZeros().toPlainString()
+ + " 鍏冦�傝璋冩暣妯℃澘涓殑銆屽紑绁ㄩ噾棰�(鍏�)銆嶏紝鎴栧厛鍦ㄧ郴缁熶腑鏍稿宸插紑绁ㄩ噾棰濆悗閲嶆柊瀵煎叆");
+ }
+
+ // 3. 鍙戠エ绫诲瀷鏍¢獙
+ Integer invoiceType = resolveInvoiceType(row.getInvoiceType());
+
+ // 4. 寮�绁ㄧ紪鍙凤細濉啓鍒欎娇鐢紝鐣欑┖鍒欒嚜鍔ㄧ敓鎴�
+ String no = StrUtil.isNotBlank(row.getNo()) ? row.getNo().trim()
+ : noRedisDAO.generate(CrmNoRedisDAO.INVOICE_PREFIX);
+ if (invoiceMapper.selectByNo(no) != null) {
+ throw new IllegalArgumentException("寮�绁ㄧ紪鍙峰凡瀛樺湪锛�" + no
+ + "銆傝淇敼妯℃澘涓殑寮�绁ㄧ紪鍙凤紝鎴栧厛鍦ㄧ郴缁熶腑鍒犻櫎璇ョ紪鍙风殑寮�绁ㄥ悗閲嶆柊瀵煎叆");
+ }
+
+ // 5. 鍘婚噸锛氬悓涓�鍚堝悓涓嬶紝鍙戠エ鍙风爜锛堢暀绌烘椂鎸夊紑绁ㄩ噾棰濄�佸彂绁ㄦ姮澶淬�佸紑绁ㄦ棩鏈燂級鐩稿悓鐨勫紑绁ㄤ笉鍙噸澶嶅鍏�
+ // 寮�绁ㄦ棩鏈熺暀绌烘椂瑙嗕綔涓嶆瘮杈冭缁村害锛屾晠鎸夆�滄槸鍚﹀~鍐欌�濅紶 null锛屼笉鑳界敤 parseImportDateTime 鐨� now() 鍏滃簳鍊�
+ LocalDateTime invoiceTime = parseImportDateTime(row.getInvoiceTime());
+ CrmInvoiceDO duplicate = findDuplicateInvoice(contract.getId(), row,
+ StrUtil.isNotBlank(row.getInvoiceTime()) ? invoiceTime : null);
+ if (duplicate != null) {
+ throw duplicateExistingException(buildInvoiceKeyDesc(row), duplicate.getNo());
+ }
+
+ // 6. 淇濆瓨寮�绁�
+ CrmInvoiceDO invoice = new CrmInvoiceDO().setNo(no).setCustomerId(contract.getCustomerId())
+ .setContractId(contract.getId()).setOwnerUserId(getLoginUserId()).setInvoiceType(invoiceType)
+ .setInvoiceNo(row.getInvoiceNo()).setInvoiceTitle(row.getInvoiceTitle()).setPrice(row.getPrice())
+ .setInvoiceTime(invoiceTime).setRemark(row.getRemark())
+ .setAuditStatus(CrmAuditStatusEnum.DRAFT.getStatus());
+ invoiceMapper.insert(invoice);
+
+ // 7. 鍒涘缓鏁版嵁鏉冮檺锛堝惁鍒欏綋鍓嶇敤鎴风湅涓嶅埌璇ュ紑绁級
+ permissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_INVOICE.getType())
+ .setBizId(invoice.getId()).setUserId(invoice.getOwnerUserId())
+ .setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
+ return invoice.getNo();
+ }
+
+ private Integer resolveInvoiceType(String text) {
+ List<String> labelList = DictFrameworkUtils.getDictDataLabelList(CRM_INVOICE_TYPE);
+ if (StrUtil.isBlank(text)) {
+ throw new IllegalArgumentException("鍙戠エ绫诲瀷涓嶈兘涓虹┖锛氳濉啓妯℃澘涓琛岀殑銆屽彂绁ㄧ被鍨嬨�嶅垪锛屽彲閫夊�硷細"
+ + String.join("銆�", labelList));
+ }
+ String value = text.trim();
+ if (value.matches("\\d+") && DictFrameworkUtils.getDictDataValueList(CRM_INVOICE_TYPE).contains(value)) {
+ return Integer.valueOf(value);
+ }
+ String parsedValue = DictFrameworkUtils.parseDictDataValue(CRM_INVOICE_TYPE, value);
+ if (parsedValue == null) {
+ throw new IllegalArgumentException("鍙戠エ绫诲瀷涓嶅瓨鍦細" + value + "銆傛ā鏉夸腑銆屽彂绁ㄧ被鍨嬨�嶈濉啓浠ヤ笅涔嬩竴锛�"
+ + String.join("銆�", labelList));
+ }
+ return Integer.valueOf(parsedValue);
+ }
+
+ private LocalDateTime parseImportDateTime(String text) {
+ if (StrUtil.isBlank(text)) {
+ return LocalDateTime.now();
+ }
+ String value = text.trim();
+ for (String pattern : new String[]{"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy/MM/dd HH:mm:ss"}) {
+ try {
+ return LocalDateTime.parse(value, DateTimeFormatter.ofPattern(pattern));
+ } catch (Exception ignored) {
+ // 缁х画灏濊瘯涓嬩竴涓牸寮�
+ }
+ }
+ for (String pattern : new String[]{"yyyy-MM-dd", "yyyy/M/d"}) {
+ try {
+ return java.time.LocalDate.parse(value, DateTimeFormatter.ofPattern(pattern)).atStartOfDay();
+ } catch (Exception ignored) {
+ // 缁х画灏濊瘯涓嬩竴涓牸寮�
+ }
+ }
+ throw new IllegalArgumentException("寮�绁ㄦ棩鏈熸牸寮忎笉姝g‘锛�" + text + "锛岀ず渚� 2026-01-01 10:00:00");
+ }
+
+ private String resolveImportErrorMessage(Exception ex) {
+ if (ex instanceof cn.iocoder.yudao.framework.common.exception.ServiceException serviceException) {
+ return serviceException.getMessage();
+ }
+ return StrUtil.blankToDefault(ex.getMessage(), "瀵煎叆澶辫触");
+ }
+
+ /**
+ * 瀵煎叆鍘婚噸閿細鍙戠エ鍙风爜濉啓鏃朵互 鍚堝悓 + 鍙戠エ鍙风爜 涓哄噯锛涚暀绌烘椂姣旇緝 鍚堝悓 + 寮�绁ㄩ噾棰� + 鍙戠エ鎶ご + 寮�绁ㄦ棩鏈�
+ */
+ private String buildInvoiceImportKey(CrmInvoiceImportExcelVO row) {
+ if (StrUtil.isNotBlank(row.getInvoiceNo())) {
+ return "no|" + StrUtil.trimToEmpty(row.getContractNo()) + "|" + row.getInvoiceNo().trim();
+ }
+ return "amt|" + StrUtil.trimToEmpty(row.getContractNo()) + "|"
+ + (row.getPrice() == null ? "" : row.getPrice().stripTrailingZeros().toPlainString()) + "|"
+ + StrUtil.trimToEmpty(row.getInvoiceTitle()) + "|" + StrUtil.trimToEmpty(row.getInvoiceTime());
+ }
+
+ private String buildInvoiceKeyDesc(CrmInvoiceImportExcelVO row) {
+ if (StrUtil.isNotBlank(row.getInvoiceNo())) {
+ return "鍚堝悓缂栧彿[" + StrUtil.trimToEmpty(row.getContractNo()) + "]銆佸彂绁ㄥ彿鐮乕" + row.getInvoiceNo().trim() + "]";
+ }
+ return "鍚堝悓缂栧彿[" + StrUtil.trimToEmpty(row.getContractNo()) + "]銆佸紑绁ㄩ噾棰漑"
+ + (row.getPrice() == null ? "鏈~鍐�" : row.getPrice().stripTrailingZeros().toPlainString() + " 鍏�")
+ + "]銆佸彂绁ㄦ姮澶碵" + StrUtil.blankToDefault(StrUtil.trimToNull(row.getInvoiceTitle()), "鏈~鍐�") + "]"
+ + (StrUtil.isBlank(row.getInvoiceTime()) ? "" : "銆佸紑绁ㄦ棩鏈焄" + row.getInvoiceTime().trim() + "]");
+ }
+
+ /**
+ * 鏌ヨ鍚屽悎鍚屼笅涓氬姟鏁版嵁瀹屽叏鐩稿悓鐨勫紑绁�
+ */
+ private CrmInvoiceDO findDuplicateInvoice(Long contractId, CrmInvoiceImportExcelVO row, LocalDateTime invoiceTime) {
+ for (CrmInvoiceDO old : invoiceMapper.selectListByContractId(contractId)) {
+ if (StrUtil.isNotBlank(row.getInvoiceNo())) {
+ if (StrUtil.equals(StrUtil.trimToEmpty(old.getInvoiceNo()), row.getInvoiceNo().trim())) {
+ return old;
+ }
+ continue;
+ }
+ // 鍙戠エ鍙风爜鐣欑┖鏃讹紝姣旇緝寮�绁ㄩ噾棰濄�佸彂绁ㄦ姮澶达紱寮�绁ㄦ棩鏈熺暀绌哄垯涓嶆瘮杈冭缁村害
+ if (old.getPrice().compareTo(row.getPrice()) != 0
+ || !StrUtil.equals(StrUtil.trimToEmpty(old.getInvoiceTitle()),
+ StrUtil.trimToEmpty(row.getInvoiceTitle()))) {
+ continue;
+ }
+ if (invoiceTime != null && !invoiceTime.equals(old.getInvoiceTime())) {
+ continue;
+ }
+ return old;
+ }
+ return null;
+ }
+
+ private IllegalArgumentException duplicateRowException(int firstRowNo, String keyDesc) {
+ return new IllegalArgumentException("妫�娴嬪埌閲嶅瀵煎叆锛氭湰娆″鍏ョ殑绗� " + firstRowNo
+ + " 琛屼笌褰撳墠琛岀殑鏁版嵁瀹屽叏鐩稿悓锛�" + keyDesc + "锛夈�傝鍒犻櫎妯℃澘涓噸澶嶇殑琛屽悗閲嶆柊瀵煎叆");
+ }
+
+ private IllegalArgumentException duplicateExistingException(String keyDesc, String existingNo) {
+ return new IllegalArgumentException("妫�娴嬪埌閲嶅瀵煎叆锛�" + keyDesc + " 涓庡凡鏈夊紑绁ㄥ崟銆�" + existingNo
+ + "銆嶅畬鍏ㄧ浉鍚岋紙鍙兘鏄湰娆″鍏ュ墠闈㈢殑琛岋紝涔熷彲鑳芥槸绯荤粺涓凡鏈夊崟鎹級銆傝鍕块噸澶嶅鍏ワ紱濡傜‘闇�閲嶆柊瀵煎叆锛岃鍏堝湪銆岃惀閿�绠$悊-寮�绁ㄧ鐞嗐�嶄腑鍒犻櫎璇ュ紑绁ㄥ崟鍚庨噸璇�");
+ }
+
private void validateInvoicePriceExceedsLimit(CrmInvoiceSaveReqVO reqVO) {
// 1. 璁$畻鍓╀綑鍙紑绁ㄩ噾棰濓紝涓嶅寘鎷� reqVO 鑷韩锛堝鎵逛腑涔熷崰鐢ㄩ搴︼紝闃叉绱寮�绁ㄨ秴杩囧悎鍚岄噾棰濓級
CrmContractDO contract = contractService.validateContract(reqVO.getContractId());
- List<CrmInvoiceDO> invoices = invoiceMapper.selectListByContractIdAndStatus(reqVO.getContractId(),
- List.of(CrmAuditStatusEnum.APPROVE.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus()));
- if (reqVO.getId() != null) {
- invoices.removeIf(invoice -> ObjectUtil.equal(invoice.getId(), reqVO.getId()));
- }
- BigDecimal notInvoicePrice = contract.getTotalPrice().subtract(
- CollectionUtils.getSumValue(invoices, CrmInvoiceDO::getPrice, BigDecimal::add, BigDecimal.ZERO));
+ BigDecimal notInvoicePrice = getRemainingInvoicePrice(contract, reqVO.getId());
// 2. 鏍¢獙閲戦鏄惁瓒呰繃
if (reqVO.getPrice().compareTo(notInvoicePrice) > 0) {
throw exception(INVOICE_CREATE_FAIL_PRICE_EXCEEDS_LIMIT, notInvoicePrice);
}
}
+ /**
+ * 璁$畻鍚堝悓鍓╀綑鍙紑绁ㄩ噾棰濓紙瀹℃壒涓�佸鏍搁�氳繃鐨勫紑绁ㄥ崰鐢ㄩ搴︼級
+ *
+ * @param contract 鍚堝悓
+ * @param excludeInvoiceId 闇�鎺掗櫎鐨勫紑绁ㄧ紪鍙凤紝鍙负绌�
+ */
+ private BigDecimal getRemainingInvoicePrice(CrmContractDO contract, Long excludeInvoiceId) {
+ List<CrmInvoiceDO> invoices = invoiceMapper.selectListByContractIdAndStatus(contract.getId(),
+ List.of(CrmAuditStatusEnum.APPROVE.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus()));
+ if (excludeInvoiceId != null) {
+ invoices.removeIf(invoice -> ObjectUtil.equal(invoice.getId(), excludeInvoiceId));
+ }
+ return contract.getTotalPrice().subtract(
+ CollectionUtils.getSumValue(invoices, CrmInvoiceDO::getPrice, BigDecimal::add, BigDecimal.ZERO));
+ }
+
private void validateRelationDataExists(CrmInvoiceSaveReqVO reqVO) {
if (reqVO.getOwnerUserId() != null) {
adminUserApi.validateUser(reqVO.getOwnerUserId()); // 鏍¢獙璐熻矗浜哄瓨鍦�
--
Gitblit v1.9.3