package org.dromara.crm.service.impl; import cn.hutool.core.date.DateUtil; import cn.hutool.json.JSONArray; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.service.UserService; import org.dromara.common.core.utils.MapstructUtils; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.crm.domain.*; import org.dromara.crm.domain.bo.CrmContractBo; import org.dromara.crm.domain.bo.CrmContractProductBo; import org.dromara.crm.domain.bo.CrmContractProductPriceTierBo; import org.dromara.crm.domain.bo.CrmProductBo; import org.dromara.crm.domain.vo.*; import org.dromara.crm.mapper.*; import org.dromara.crm.service.ICrmContractApprovalService; import org.dromara.crm.service.ICrmContractProductService; import org.dromara.crm.service.ICrmContractService; import org.dromara.crm.service.ICrmPaymentPlanService; import org.dromara.crm.service.ICrmProductService; import org.dromara.crm.service.ICrmSupplierInfoService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; /** * 合同管理Service实现 * * @author AI Assistant * @date 2025-10-04 */ @Slf4j @RequiredArgsConstructor @Service public class CrmContractServiceImpl implements ICrmContractService { private final CrmContractMapper baseMapper; private final ICrmContractProductService contractProductService; private final CrmContractApprovalMapper approvalMapper; private final ICrmContractApprovalService contractApprovalService; private final ICrmPaymentPlanService paymentPlanService; private final CrmPaymentMapper paymentMapper; private final CrmOpportunityMapper opportunityMapper; private final CrmQuoteMapper quoteMapper; private final CrmContractProductMapper contractProductMapper; private final CrmContractTemplateMapper contractTemplateMapper; private final CrmInvoiceMapper invoiceMapper; private final CrmCustomerMapper customerMapper; private final CrmContactMapper contactMapper; private final UserService userService; private final ICrmSupplierInfoService supplierInfoService; private final ICrmProductService productService; private final CrmProductMapper productMapper; private final CrmContractProductPriceTierMapper contractProductPriceTierMapper; /** * 构建查询条件 */ private LambdaQueryWrapper buildQueryWrapper(CrmContractBo bo) { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); // 合同编号、合同名称和客户名称的OR查询 // 如果三个字段都有值且相同,则认为是联合搜索(OR关系) if (StringUtils.isNotBlank(bo.getContractNo()) && StringUtils.isNotBlank(bo.getContractName()) && StringUtils.isNotBlank(bo.getCustomerName()) && bo.getContractNo().equals(bo.getContractName()) && bo.getContractName().equals(bo.getCustomerName())) { // 合同编号、合同名称或客户名称包含关键字 String keyword = bo.getContractNo(); lqw.and(wrapper -> wrapper .like(CrmContract::getContractNo, keyword) .or() .like(CrmContract::getContractName, keyword) .or() .like(CrmContract::getCustomerName, keyword)); } else { // 分别查询 lqw.like(StringUtils.isNotBlank(bo.getContractNo()), CrmContract::getContractNo, bo.getContractNo()); lqw.like(StringUtils.isNotBlank(bo.getContractName()), CrmContract::getContractName, bo.getContractName()); lqw.like(StringUtils.isNotBlank(bo.getCustomerName()), CrmContract::getCustomerName, bo.getCustomerName()); } lqw.eq(bo.getCustomerId() != null, CrmContract::getCustomerId, bo.getCustomerId()); lqw.eq(bo.getOpportunityId() != null, CrmContract::getOpportunityId, bo.getOpportunityId()); lqw.eq(bo.getQuoteId() != null, CrmContract::getQuoteId, bo.getQuoteId()); lqw.eq(StringUtils.isNotBlank(bo.getContractType()), CrmContract::getContractType, bo.getContractType()); lqw.eq(StringUtils.isNotBlank(bo.getStatus()), CrmContract::getStatus, bo.getStatus()); // 排除指定状态(如默认排除已作废的记录) lqw.ne(StringUtils.isNotBlank(bo.getExcludeStatus()), CrmContract::getStatus, bo.getExcludeStatus()); lqw.eq(StringUtils.isNotBlank(bo.getApprovalStatus()), CrmContract::getApprovalStatus, bo.getApprovalStatus()); lqw.eq(bo.getOwnerUserId() != null, CrmContract::getOwnerUserId, bo.getOwnerUserId()); // 如果指定了待审批人ID,查询该用户待审批的合同 if (bo.getPendingApprovalUserId() != null) { // 使用SQL子查询:查询审批记录中审批人是当前用户且审批结果是pending且isCurrent=1的合同 lqw.inSql(CrmContract::getId, "SELECT DISTINCT contract_id FROM crm_contract_approval " + "WHERE approver_id = " + bo.getPendingApprovalUserId() + " AND approval_result = 'pending' " + " AND is_current = 1 " + " AND del_flag = 0"); } // 如果指定了审批人ID,查询该用户审批过的合同 if (bo.getApprovedByUserId() != null) { // 查询该审批人已审批过的合同ID列表 LambdaQueryWrapper approvalLqw = Wrappers.lambdaQuery(); approvalLqw.eq(CrmContractApproval::getApproverId, bo.getApprovedByUserId()); approvalLqw.in(CrmContractApproval::getApprovalResult, "approved", "rejected"); List approvalList = approvalMapper.selectList(approvalLqw); if (!approvalList.isEmpty()) { List contractIds = approvalList.stream() .map(CrmContractApproval::getContractId) .distinct() .collect(java.util.stream.Collectors.toList()); lqw.in(CrmContract::getId, contractIds); } else { // 如果没有审批记录,返回空结果 lqw.eq(CrmContract::getId, -1L); } } lqw.ge(bo.getParams().get("minAmount") != null, CrmContract::getAmount, bo.getParams().get("minAmount")); lqw.le(bo.getParams().get("maxAmount") != null, CrmContract::getAmount, bo.getParams().get("maxAmount")); // 签订日期范围查询 if (bo.getSignDateBegin() != null && !bo.getSignDateBegin().isEmpty()) { lqw.ge(CrmContract::getSignDate, LocalDate.parse(bo.getSignDateBegin())); } if (bo.getSignDateEnd() != null && !bo.getSignDateEnd().isEmpty()) { lqw.le(CrmContract::getSignDate, LocalDate.parse(bo.getSignDateEnd())); } lqw.between(bo.getParams().get("beginTime") != null && bo.getParams().get("endTime") != null, CrmContract::getCreateTime, bo.getParams().get("beginTime"), bo.getParams().get("endTime")); lqw.orderByDesc(CrmContract::getCreateTime); return lqw; } @Override public TableDataInfo queryPageList(CrmContractBo bo, PageQuery pageQuery) { LambdaQueryWrapper lqw = buildQueryWrapper(bo); Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); // 设置每个合同的审批权限 try { Long currentUserId = LoginHelper.getUserId(); for (CrmContractVo vo : result.getRecords()) { vo.setCanApprove(contractApprovalService.canUserApprove(vo.getId(), currentUserId)); } } catch (Exception e) { log.warn("获取当前用户审批权限失败: {}", e.getMessage()); } return TableDataInfo.build(result); } @Override public List queryList(CrmContractBo bo) { LambdaQueryWrapper lqw = buildQueryWrapper(bo); return baseMapper.selectVoList(lqw); } @Override public CrmContractVo queryById(Long id) { CrmContractVo vo = baseMapper.selectVoById(id); if (vo != null) { // 设置当前用户是否可以审批 try { Long currentUserId = LoginHelper.getUserId(); vo.setCanApprove(contractApprovalService.canUserApprove(id, currentUserId)); } catch (Exception e) { log.warn("获取当前用户审批权限失败: {}", e.getMessage()); vo.setCanApprove(false); } // 关联查询供方主体名称 if (vo.getSupplierId() != null) { try { CrmSupplierInfoVo supplierInfo = supplierInfoService.queryById(vo.getSupplierId()); if (supplierInfo != null) { vo.setSupplierName(supplierInfo.getSupplierName()); } } catch (Exception e) { log.warn("获取供方主体信息失败: {}", e.getMessage()); } } // 查询并设置产品明细列表 try { List products = contractProductService.queryListByContractId(id); if (vo.getProducts() == null || vo.getProducts().isEmpty()) { vo.setProducts(products); } if ("framework".equals(vo.getContractType()) && products != null && !products.isEmpty()) { List tierEntities = contractProductPriceTierMapper.selectList( Wrappers.lambdaQuery(CrmContractProductPriceTier.class) .eq(CrmContractProductPriceTier::getContractId, id) .orderByAsc(CrmContractProductPriceTier::getContractProductId) .orderByAsc(CrmContractProductPriceTier::getSort)); List allTiers = new ArrayList<>(); for (CrmContractProductPriceTier en : tierEntities) { allTiers.add(convertTierEntityToVo(en)); } Map> tierMap = new HashMap<>(); for (CrmContractProductPriceTierVo t : allTiers) { tierMap.computeIfAbsent(t.getContractProductId(), k -> new ArrayList<>()).add(t); } for (CrmContractProductVo p : products) { p.setPriceTiers(tierMap.getOrDefault(p.getId(), new ArrayList<>())); } } log.info("查询合同详情成功,合同ID: {}, 产品数量: {}", id, products != null ? products.size() : 0); } catch (Exception e) { log.error("查询合同产品明细失败,合同ID: {}", id, e); } } return vo; } @Override @Transactional(rollbackFor = Exception.class) public Boolean insertByBo(CrmContractBo bo) { validateContractTypeAndProductsPresence(bo); normalizeFrameworkContractData(bo); validateProductPricesByContractType(bo); validateSalePurchaseContractAmount(bo); CrmContract entity = MapstructUtils.convert(bo, CrmContract.class); // 查询供方信息(用于生成合同编号和合同名称) String supplierShortName = "山峡"; // 默认简称 if (entity.getSupplierId() != null) { try { CrmSupplierInfoVo supplierInfo = supplierInfoService.queryById(entity.getSupplierId()); if (supplierInfo != null && StringUtils.isNotBlank(supplierInfo.getShortName())) { supplierShortName = supplierInfo.getShortName(); } } catch (Exception e) { log.warn("查询供方信息失败,使用默认简称,供方ID: {}", entity.getSupplierId(), e); } } // 生成合同编号 if (StringUtils.isBlank(entity.getContractNo())) { entity.setContractNo(generateContractNo(entity.getSupplierId(), entity.getSignDate())); } // 查询客户信息 CrmCustomer customer = null; if (entity.getCustomerId() != null) { customer = customerMapper.selectById(entity.getCustomerId()); if (customer != null) { entity.setCustomerName(customer.getCustomerName()); } } String contractType = entity.getContractType(); // 自动生成合同名称 String contractNo = entity.getContractNo(); String customerShortName = customer != null ? customer.getShortName() : ""; entity.setContractName(generateContractName(contractNo, contractType, supplierShortName, customerShortName, entity.getContractName())); // 查询并设置联系人信息 if (entity.getContactId() != null) { CrmContact contact = contactMapper.selectById(entity.getContactId()); if (contact != null) { entity.setContactName(contact.getContactName()); entity.setContactMobile(contact.getMobile()); } } // 设置默认值 if (entity.getStatus() == null) { entity.setStatus("draft"); } // 草稿状态下,审批状态应该为空,只有提交审批后才设置为pending if ("draft".equals(entity.getStatus())) { entity.setApprovalStatus(null); } if (entity.getOwnerUserId() == null) { entity.setOwnerUserId(LoginHelper.getUserId()); } // 设置负责人姓名 if (entity.getOwnerUserId() != null) { try { String ownerUserName = userService.selectNicknameById(entity.getOwnerUserId()); entity.setOwnerUserName(ownerUserName); } catch (Exception e) { log.warn("获取负责人姓名失败,用户ID: {}", entity.getOwnerUserId(), e); entity.setOwnerUserName(null); } } else { entity.setOwnerUserName(null); } if (entity.getReceivedAmount() == null) { entity.setReceivedAmount(BigDecimal.ZERO); } if (entity.getInvoiceAmount() == null) { entity.setInvoiceAmount(BigDecimal.ZERO); } // 计算未回款金额和未开票金额 entity.setUnreceivedAmount(entity.getAmount().subtract(entity.getReceivedAmount())); entity.setUninvoiceAmount(entity.getAmount().subtract(entity.getInvoiceAmount())); // 保存合同 resolveContractProductIds(bo.getProducts()); boolean flag = baseMapper.insert(entity) > 0; if (flag) { bo.setId(entity.getId()); // 保存产品明细 if (bo.getProducts() != null && !bo.getProducts().isEmpty()) { int sort = 1; for (CrmContractProductBo product : bo.getProducts()) { product.setContractId(entity.getId()); // 清空ID,让MyBatis-Plus自动生成新ID,避免主键冲突 product.setId(null); // 如果没有设置排序号,则自动设置为循环索引 if (product.getSort() == null) { product.setSort(sort); } sort++; contractProductService.insertByBo(product); if ("framework".equals(entity.getContractType())) { savePriceTiersForProduct(product.getId(), entity.getId(), product.getPriceTiers()); } } } // 生成回款计划 if (bo.getPaymentPlans() != null && !bo.getPaymentPlans().isEmpty()) { try { paymentPlanService.batchGenerate(entity.getId(), bo.getPaymentPlans()); log.info("合同创建成功,已生成回款计划,合同ID: {}, 期数: {}", entity.getId(), bo.getPaymentPlans().size()); } catch (Exception e) { log.error("生成回款计划失败,合同ID: {}", entity.getId(), e); // 不影响合同创建,只记录日志 } } } return flag; } @Override @Transactional(rollbackFor = Exception.class) public Boolean updateByBo(CrmContractBo bo) { // 只有草稿状态的合同才能修改 CrmContract existContract = baseMapper.selectById(bo.getId()); if (existContract == null) { throw new ServiceException("合同不存在"); } if (!"draft".equals(existContract.getStatus())) { throw new ServiceException("只有草稿状态的合同才能修改"); } validateContractTypeAndProductsPresence(bo); normalizeFrameworkContractData(bo); validateProductPricesByContractType(bo); validateSalePurchaseContractAmount(bo); CrmContract entity = MapstructUtils.convert(bo, CrmContract.class); // 草稿状态下,审批状态应该为空 if ("draft".equals(existContract.getStatus())) { entity.setApprovalStatus(null); } // 查询并设置客户名称(如果客户ID有变化) CrmCustomer customer = null; if (entity.getCustomerId() != null && !entity.getCustomerId().equals(existContract.getCustomerId())) { customer = customerMapper.selectById(entity.getCustomerId()); if (customer != null) { entity.setCustomerName(customer.getCustomerName()); } } else if (entity.getCustomerId() != null) { // 客户ID没变化,但可能需要用于生成合同名称 customer = customerMapper.selectById(entity.getCustomerId()); } // 查询并设置联系人信息(如果联系人ID有变化) if (entity.getContactId() != null && !entity.getContactId().equals(existContract.getContactId())) { CrmContact contact = contactMapper.selectById(entity.getContactId()); if (contact != null) { entity.setContactName(contact.getContactName()); entity.setContactMobile(contact.getMobile()); } } // 检查签订日期是否变化,如果变化则重新生成合同编号和合同名称 boolean signDateChanged = entity.getSignDate() != null && !entity.getSignDate().equals(existContract.getSignDate()); boolean supplierIdChanged = entity.getSupplierId() != null && !entity.getSupplierId().equals(existContract.getSupplierId()); if (signDateChanged || supplierIdChanged) { // 查询供方信息(用于生成合同编号和合同名称) String supplierShortName = "山峡"; // 默认简称 if (entity.getSupplierId() != null) { try { CrmSupplierInfoVo supplierInfo = supplierInfoService.queryById(entity.getSupplierId()); if (supplierInfo != null && StringUtils.isNotBlank(supplierInfo.getShortName())) { supplierShortName = supplierInfo.getShortName(); } } catch (Exception e) { log.warn("查询供方信息失败,使用默认简称,供方ID: {}", entity.getSupplierId(), e); } } // 重新生成合同编号 entity.setContractNo(generateContractNo(entity.getSupplierId(), entity.getSignDate())); // 重新生成合同名称 String contractNo = entity.getContractNo(); String customerShortName = customer != null ? customer.getShortName() : ""; // 判断用户是否修改了合同名称 // 如果用户修改了合同名称,使用用户的新名称;否则使用旧名称并更新编号 String userContractName = null; if (StringUtils.isNotBlank(entity.getContractName()) && !entity.getContractName().equals(existContract.getContractName())) { // 用户修改了合同名称,使用用户的新名称(移除可能存在的旧编号) userContractName = entity.getContractName().replaceAll("([^)]+)$", ""); } else { // 用户没有修改合同名称,使用旧名称(移除旧的合同编号) String oldContractName = existContract.getContractName(); if (StringUtils.isNotBlank(oldContractName)) { userContractName = oldContractName.replaceAll("([^)]+)$", ""); } } entity.setContractName(generateContractName(contractNo, entity.getContractType(), supplierShortName, customerShortName, userContractName)); } // 重新计算未回款金额和未开票金额 if (entity.getAmount() != null) { BigDecimal receivedAmount = existContract.getReceivedAmount() != null ? existContract.getReceivedAmount() : BigDecimal.ZERO; BigDecimal invoiceAmount = existContract.getInvoiceAmount() != null ? existContract.getInvoiceAmount() : BigDecimal.ZERO; entity.setUnreceivedAmount(entity.getAmount().subtract(receivedAmount)); entity.setUninvoiceAmount(entity.getAmount().subtract(invoiceAmount)); } boolean flag = baseMapper.updateById(entity) > 0; if (flag && bo.getProducts() != null) { resolveContractProductIds(bo.getProducts()); contractProductPriceTierMapper.delete(Wrappers.lambdaQuery(CrmContractProductPriceTier.class) .eq(CrmContractProductPriceTier::getContractId, bo.getId())); // 更新产品明细(简化处理:先删除再新增) contractProductService.deleteByIds( contractProductService.queryListByContractId(bo.getId()) .stream().map(p -> p.getId()).toList()); int sort = 1; for (CrmContractProductBo product : bo.getProducts()) { product.setContractId(bo.getId()); // 清空ID,让MyBatis-Plus自动生成新ID,避免主键冲突 product.setId(null); // 如果没有设置排序号,则自动设置为循环索引 if (product.getSort() == null) { product.setSort(sort); } sort++; contractProductService.insertByBo(product); if ("framework".equals(bo.getContractType())) { savePriceTiersForProduct(product.getId(), bo.getId(), product.getPriceTiers()); } } } return flag; } @Override @Transactional(rollbackFor = Exception.class) public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { if (isValid) { // 校验:只能删除草稿状态的合同 for (Long id : ids) { CrmContract contract = baseMapper.selectById(id); if (contract != null && !"draft".equals(contract.getStatus())) { throw new ServiceException("只能删除草稿状态的合同"); } } } return baseMapper.deleteByIds(ids) > 0; } @Override @Transactional(rollbackFor = Exception.class) public Boolean submitApproval(Long id, List approverIds, String remark) { CrmContract contract = baseMapper.selectById(id); if (contract == null) { throw new ServiceException("合同不存在"); } if (!"draft".equals(contract.getStatus())) { throw new ServiceException("只有草稿状态的合同才能提交审批"); } validateFrameworkContractForSubmitApproval(id, contract); // 更新合同状态 CrmContract update = new CrmContract(); update.setId(id); update.setStatus("approving"); update.setApprovalStatus("pending"); baseMapper.updateById(update); // 创建审批记录 if (approverIds != null && !approverIds.isEmpty()) { for (int i = 0; i < approverIds.size(); i++) { Long approverId = approverIds.get(i); CrmContractApproval approval = new CrmContractApproval(); approval.setContractId(id); approval.setStep(i + 1); approval.setStepName("第" + (i + 1) + "步审批"); approval.setApproverId(approverId); // TODO: 这里应该查询用户信息填充审批人姓名和部门 // 由于跨模块调用复杂性,暂时在审批时填充 // 或者可以通过远程API调用System模块的用户服务 approval.setApproverName("审批人" + approverId); // 临时占位 approval.setApproverDept("待审批时填充"); // 临时占位 approval.setApprovalResult("pending"); approval.setIsCurrent(i == 0 ? 1 : 0); approval.setSort(i + 1); approvalMapper.insert(approval); } } return true; } @Override @Transactional(rollbackFor = Exception.class) public int batchSubmitApproval(List ids, List approverIds, String remark) { int successCount = 0; for (Long id : ids) { try { submitApproval(id, approverIds, remark); successCount++; } catch (Exception e) { // 记录错误但继续处理其他合同 log.error("合同ID {} 提交审批失败: {}", id, e.getMessage()); } } return successCount; } @Override public Boolean saveTemplate(Long id, String templateContent, String settlementClauseDrafts) { // 检查合同是否存在 CrmContract entity = baseMapper.selectById(id); if (entity == null) { throw new ServiceException("合同不存在"); } String finalSettlementClauseDrafts = settlementClauseDrafts; if (StringUtils.isBlank(finalSettlementClauseDrafts)) { String method = StringUtils.isNotBlank(entity.getSettlementMethod()) ? entity.getSettlementMethod() : "arrive_payment"; String defaultClause = "advance_payment".equals(method) ? "款到发货。需方付款后安排发货,需方收到货物后,供方开具13%增值税专用发票。" : "货到付款,需方收到供方货物后,供方开具13%增值税专用发票,无异议后,以现汇方式月结。"; JSONObject drafts = new JSONObject(); drafts.set(method, defaultClause); finalSettlementClauseDrafts = drafts.toString(); } log.info("========== 开始保存合同模板内容 =========="); log.info("contractId={}", id); log.info("contentLength={}", templateContent != null ? templateContent.length() : 0); log.info("content前200字符={}", templateContent != null && templateContent.length() > 200 ? templateContent.substring(0, 200) : templateContent); log.info("包含HTML标签={}", templateContent != null && templateContent.contains("<")); log.info("第五条草稿长度={}", finalSettlementClauseDrafts != null ? finalSettlementClauseDrafts.length() : 0); // 使用 LambdaUpdateWrapper 只更新模板相关字段,避免乐观锁问题 int result = baseMapper.update(null, Wrappers.lambdaUpdate() .eq(CrmContract::getId, id) .set(CrmContract::getTemplateContent, templateContent) .set(CrmContract::getSettlementClauseDrafts, finalSettlementClauseDrafts)); log.info("保存合同模板内容完成: contractId={}, success={}, affectedRows={}", id, result > 0, result); // 验证保存结果 CrmContract savedEntity = baseMapper.selectById(id); if (savedEntity != null && savedEntity.getTemplateContent() != null) { log.info("验证保存结果: 数据库中的内容长度={}", savedEntity.getTemplateContent().length()); log.info("验证保存结果: 数据库中的内容前200字符={}", savedEntity.getTemplateContent().length() > 200 ? savedEntity.getTemplateContent().substring(0, 200) : savedEntity.getTemplateContent()); } else { log.warn("验证保存结果: 数据库中未找到模板内容!"); } return result > 0; } @Override public String getTemplate(Long id) { log.info("========== 开始获取合同模板内容 =========="); log.info("contractId={}", id); // 直接查询,确保获取最新数据 CrmContract entity = baseMapper.selectById(id); if (entity == null) { throw new ServiceException("合同不存在"); } String templateContent = entity.getTemplateContent(); log.info("获取合同模板内容完成:"); log.info("- hasTemplate={}", templateContent != null); log.info("- contentLength={}", templateContent != null ? templateContent.length() : 0); log.info("- content前200字符={}", templateContent != null && templateContent.length() > 200 ? templateContent.substring(0, 200) : templateContent); log.info("- 包含HTML标签={}", templateContent != null && templateContent.contains("<")); return templateContent; } @Override @Transactional(rollbackFor = Exception.class) public Boolean withdrawApproval(Long id, String remark) { CrmContract contract = baseMapper.selectById(id); if (contract == null) { throw new ServiceException("合同不存在"); } if (!"approving".equals(contract.getStatus())) { throw new ServiceException("只有审批中的合同才能撤回"); } // 使用 UpdateWrapper 来明确更新 null 值 com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper updateWrapper = Wrappers .lambdaUpdate(); updateWrapper.eq(CrmContract::getId, id) .set(CrmContract::getStatus, "draft") .set(CrmContract::getApprovalStatus, null); // 明确设置为 null baseMapper.update(null, updateWrapper); // 删除待审批的审批记录 LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); wrapper.eq(CrmContractApproval::getContractId, id); wrapper.eq(CrmContractApproval::getApprovalResult, "pending"); approvalMapper.delete(wrapper); log.info("撤回审批成功,合同ID: {}, 撤回原因: {}", id, remark); return true; } @Override @Transactional(rollbackFor = Exception.class) public Boolean startExecute(Long id, String remark) { CrmContract contract = baseMapper.selectById(id); if (contract == null) { throw new ServiceException("合同不存在"); } if (!"approved".equals(contract.getStatus())) { throw new ServiceException("只有已审批的合同才能开始执行"); } CrmContract update = new CrmContract(); update.setId(id); update.setStatus("executing"); return baseMapper.updateById(update) > 0; } @Override @Transactional(rollbackFor = Exception.class) public Boolean finishContract(Long id, String remark) { CrmContract contract = baseMapper.selectById(id); if (contract == null) { throw new ServiceException("合同不存在"); } if (!"executing".equals(contract.getStatus())) { throw new ServiceException("只有执行中的合同才能完成"); } CrmContract update = new CrmContract(); update.setId(id); update.setStatus("finished"); if (StringUtils.isNotBlank(remark)) { update.setRemark(remark); } return baseMapper.updateById(update) > 0; } @Override @Transactional(rollbackFor = Exception.class) public Boolean invalidContract(Long id, String invalidReason, String remark) { if (StringUtils.isBlank(invalidReason)) { throw new ServiceException("作废原因不能为空"); } CrmContract contract = baseMapper.selectById(id); if (contract == null) { throw new ServiceException("合同不存在"); } if ("finished".equals(contract.getStatus())) { throw new ServiceException("已完成的合同不能作废"); } CrmContract update = new CrmContract(); update.setId(id); update.setStatus("invalid"); update.setInvalidReason(invalidReason); update.setInvalidTime(LocalDateTime.now()); update.setRemark(remark); return baseMapper.updateById(update) > 0; } @Override public Boolean uploadFile(Long id, String fileUrl) { CrmContract update = new CrmContract(); update.setId(id); update.setContractFile(fileUrl); return baseMapper.updateById(update) > 0; } @Override @Transactional(rollbackFor = Exception.class) public Boolean uploadAttachments(Long id, String ossIds) { if (StringUtils.isBlank(ossIds)) { throw new ServiceException("附件ossId不能为空"); } CrmContract contract = baseMapper.selectById(id); if (contract == null) { throw new ServiceException("合同不存在"); } // 追加附件ossId String existingFiles = contract.getAttachmentFiles(); String newFiles; if (StringUtils.isBlank(existingFiles)) { newFiles = ossIds; } else { // 将现有附件和新附件合并(去重) String[] existingArr = existingFiles.split(","); String[] newArr = ossIds.split(","); java.util.Set fileSet = new java.util.HashSet<>(java.util.Arrays.asList(existingArr)); fileSet.addAll(java.util.Arrays.asList(newArr)); newFiles = String.join(",", fileSet); } CrmContract update = new CrmContract(); update.setId(id); update.setAttachmentFiles(newFiles); return baseMapper.updateById(update) > 0; } @Override @Transactional(rollbackFor = Exception.class) public Boolean uploadAttachmentsWithCategory(Long id, java.util.List> attachments) { if (attachments == null || attachments.isEmpty()) { throw new ServiceException("附件列表不能为空"); } CrmContract contract = baseMapper.selectById(id); if (contract == null) { throw new ServiceException("合同不存在"); } // 解析现有附件(支持JSON格式和逗号分隔格式) JSONArray existingAttachments = new JSONArray(); String existingFiles = contract.getAttachmentFiles(); if (StringUtils.isNotBlank(existingFiles)) { // 尝试解析为JSON格式 if (existingFiles.trim().startsWith("[")) { existingAttachments = JSONUtil.parseArray(existingFiles); } else { // 兼容旧格式:逗号分隔的ossId,转换为JSON格式 String[] ossIds = existingFiles.split(","); for (String ossId : ossIds) { if (StringUtils.isNotBlank(ossId)) { JSONObject item = new JSONObject(); item.set("ossId", ossId.trim()); item.set("category", "other"); // 旧数据默认分类为"其他附件" existingAttachments.add(item); } } } } // 添加新附件(去重:基于ossId) java.util.Set existingOssIds = new java.util.HashSet<>(); for (int i = 0; i < existingAttachments.size(); i++) { JSONObject item = existingAttachments.getJSONObject(i); String ossId = item.getStr("ossId"); if (StringUtils.isNotBlank(ossId)) { existingOssIds.add(ossId); } } for (java.util.Map attachment : attachments) { String ossId = attachment.get("ossId") != null ? attachment.get("ossId").toString() : null; String category = attachment.get("category") != null ? attachment.get("category").toString() : "other"; if (StringUtils.isBlank(ossId)) { continue; } // 如果已存在,更新分类;否则添加新附件 if (existingOssIds.contains(ossId)) { // 更新现有附件的分类 for (int i = 0; i < existingAttachments.size(); i++) { JSONObject item = existingAttachments.getJSONObject(i); if (ossId.equals(item.getStr("ossId"))) { item.set("category", category); break; } } } else { // 添加新附件 JSONObject newItem = new JSONObject(); newItem.set("ossId", ossId); newItem.set("category", category); existingAttachments.add(newItem); existingOssIds.add(ossId); } } // 保存为JSON格式 String newFiles = JSONUtil.toJsonStr(existingAttachments); CrmContract update = new CrmContract(); update.setId(id); update.setAttachmentFiles(newFiles); return baseMapper.updateById(update) > 0; } @Override public Map statisticsOverview(Long ownerUserId, String beginTime, String endTime) { Map result = new HashMap<>(); // 构建查询条件 LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); // 负责人筛选 if (ownerUserId != null) { lqw.eq(CrmContract::getOwnerUserId, ownerUserId); } // 时间范围筛选 if (StringUtils.isNotBlank(beginTime) && StringUtils.isNotBlank(endTime)) { lqw.between(CrmContract::getCreateTime, beginTime, endTime); } // 查询所有合同 List contracts = baseMapper.selectList(lqw); // 统计各状态数量 long totalCount = contracts.size(); long draftCount = contracts.stream().filter(c -> "draft".equals(c.getStatus())).count(); long approvingCount = contracts.stream().filter(c -> "approving".equals(c.getStatus())).count(); long executingCount = contracts.stream().filter(c -> "executing".equals(c.getStatus())).count(); long finishedCount = contracts.stream().filter(c -> "finished".equals(c.getStatus())).count(); long invalidCount = contracts.stream().filter(c -> "invalid".equals(c.getStatus())).count(); // 统计金额 BigDecimal totalAmount = contracts.stream() .map(CrmContract::getAmount) .filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal receivedAmount = contracts.stream() .map(CrmContract::getReceivedAmount) .filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal unreceivedAmount = contracts.stream() .map(CrmContract::getUnreceivedAmount) .filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal invoiceAmount = contracts.stream() .map(CrmContract::getInvoiceAmount) .filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal uninvoiceAmount = contracts.stream() .map(CrmContract::getUninvoiceAmount) .filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add); // 计算回款率和开票率 BigDecimal receivedRate = totalAmount.compareTo(BigDecimal.ZERO) > 0 ? receivedAmount.divide(totalAmount, 4, RoundingMode.HALF_UP).multiply(new BigDecimal("100")) : BigDecimal.ZERO; BigDecimal invoiceRate = totalAmount.compareTo(BigDecimal.ZERO) > 0 ? invoiceAmount.divide(totalAmount, 4, RoundingMode.HALF_UP).multiply(new BigDecimal("100")) : BigDecimal.ZERO; // 组装结果 result.put("totalCount", totalCount); result.put("draftCount", draftCount); result.put("approvingCount", approvingCount); result.put("executingCount", executingCount); result.put("finishedCount", finishedCount); result.put("invalidCount", invalidCount); result.put("totalAmount", totalAmount); result.put("receivedAmount", receivedAmount); result.put("unreceivedAmount", unreceivedAmount); result.put("receivedRate", receivedRate.setScale(2, RoundingMode.HALF_UP)); result.put("invoiceAmount", invoiceAmount); result.put("uninvoiceAmount", uninvoiceAmount); result.put("invoiceRate", invoiceRate.setScale(2, RoundingMode.HALF_UP)); return result; } @Override public List> statisticsByStatus() { List> result = new ArrayList<>(); // 查询所有合同 List contracts = baseMapper.selectList(Wrappers.lambdaQuery()); // 计算总数和总金额 long totalCount = contracts.size(); BigDecimal totalAmount = contracts.stream() .map(CrmContract::getAmount) .filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add); // 定义状态映射 Map statusMap = new LinkedHashMap<>(); statusMap.put("draft", "草稿"); statusMap.put("approving", "审批中"); statusMap.put("approved", "已审批"); statusMap.put("executing", "执行中"); statusMap.put("finished", "已完成"); statusMap.put("invalid", "已作废"); // 按状态统计 for (Map.Entry entry : statusMap.entrySet()) { String status = entry.getKey(); String statusName = entry.getValue(); List statusContracts = contracts.stream() .filter(c -> status.equals(c.getStatus())) .toList(); long count = statusContracts.size(); BigDecimal amount = statusContracts.stream() .map(CrmContract::getAmount) .filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add); // 计算百分比 BigDecimal percent = totalCount > 0 ? new BigDecimal(count).divide(new BigDecimal(totalCount), 4, RoundingMode.HALF_UP) .multiply(new BigDecimal("100")) : BigDecimal.ZERO; Map item = new HashMap<>(); item.put("status", status); item.put("statusName", statusName); item.put("count", count); item.put("amount", amount); item.put("percent", percent.setScale(2, RoundingMode.HALF_UP)); result.add(item); } return result; } @Override public List> statisticsByType() { List> result = new ArrayList<>(); // 查询所有合同 List contracts = baseMapper.selectList(Wrappers.lambdaQuery()); // 计算总数和总金额 long totalCount = contracts.size(); BigDecimal totalAmount = contracts.stream() .map(CrmContract::getAmount) .filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add); // 定义类型映射 Map typeMap = new LinkedHashMap<>(); typeMap.put("sale", "销售合同"); typeMap.put("service", "服务合同"); typeMap.put("purchase", "采购合同"); typeMap.put("framework", "框架合同"); typeMap.put("supplement", "补充协议"); typeMap.put("lease", "租赁合同"); typeMap.put("other", "其他"); // 按类型统计 for (Map.Entry entry : typeMap.entrySet()) { String type = entry.getKey(); String typeName = entry.getValue(); List typeContracts = contracts.stream() .filter(c -> type.equals(c.getContractType())) .toList(); long count = typeContracts.size(); // 如果该类型没有合同,跳过 if (count == 0) { continue; } BigDecimal amount = typeContracts.stream() .map(CrmContract::getAmount) .filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add); // 计算百分比 BigDecimal percent = totalCount > 0 ? new BigDecimal(count).divide(new BigDecimal(totalCount), 4, RoundingMode.HALF_UP) .multiply(new BigDecimal("100")) : BigDecimal.ZERO; Map item = new HashMap<>(); item.put("contractType", type); item.put("typeName", typeName); item.put("count", count); item.put("amount", amount); item.put("percent", percent.setScale(2, RoundingMode.HALF_UP)); result.add(item); } return result; } @Override public List> statisticsTrend(String type, String beginTime, String endTime) { List> result = new ArrayList<>(); // 解析开始和结束时间 LocalDateTime startDateTime; LocalDateTime endDateTime; DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); try { startDateTime = LocalDateTime.parse(beginTime, formatter); endDateTime = LocalDateTime.parse(endTime, formatter); } catch (Exception e) { log.error("日期格式错误", e); return result; } // 查询时间范围内的合同 LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.between(CrmContract::getCreateTime, startDateTime, endDateTime); List contracts = baseMapper.selectList(lqw); // 根据type确定统计粒度 String period; if ("month".equals(type)) { period = "month"; } else if ("quarter".equals(type)) { period = "quarter"; } else if ("year".equals(type)) { period = "year"; } else { period = "month"; // 默认按月统计 } // 按时间分组统计 Map> groupedContracts = new LinkedHashMap<>(); for (CrmContract contract : contracts) { if (contract.getCreateTime() == null) { continue; } // 将Date转换为LocalDateTime LocalDateTime createTime = DateUtil.toLocalDateTime(contract.getCreateTime()); String periodKey; if ("month".equals(period)) { periodKey = createTime.format(DateTimeFormatter.ofPattern("yyyy-MM")); } else if ("quarter".equals(period)) { int quarter = (createTime.getMonthValue() - 1) / 3 + 1; periodKey = createTime.getYear() + "-Q" + quarter; } else { // year periodKey = String.valueOf(createTime.getYear()); } groupedContracts.computeIfAbsent(periodKey, k -> new ArrayList<>()).add(contract); } // 生成统计结果 for (Map.Entry> entry : groupedContracts.entrySet()) { String periodKey = entry.getKey(); List periodContracts = entry.getValue(); long totalCount = periodContracts.size(); BigDecimal totalAmount = periodContracts.stream() .map(CrmContract::getAmount) .filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal receivedAmount = periodContracts.stream() .map(CrmContract::getReceivedAmount) .filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add); // 计算回款率 BigDecimal receivedRate = totalAmount.compareTo(BigDecimal.ZERO) > 0 ? receivedAmount.divide(totalAmount, 4, RoundingMode.HALF_UP).multiply(new BigDecimal("100")) : BigDecimal.ZERO; Map item = new HashMap<>(); item.put("period", periodKey); item.put("totalCount", totalCount); item.put("totalAmount", totalAmount); item.put("receivedAmount", receivedAmount); item.put("receivedRate", receivedRate.setScale(2, RoundingMode.HALF_UP)); result.add(item); } return result; } @Override public List queryExpiring(Integer days) { if (days == null || days <= 0) { days = 30; } LocalDate endDate = LocalDate.now().plusDays(days); LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.eq(CrmContract::getStatus, "executing"); lqw.le(CrmContract::getEndDate, endDate); lqw.ge(CrmContract::getEndDate, LocalDate.now()); lqw.orderByAsc(CrmContract::getEndDate); return baseMapper.selectVoList(lqw); } /** * 生成合同名称 *

销售合同、框架合同:供方简称-客户简称(合同编号);采购合同:客户简称-供方简称(合同编号);其余类型未填名称时为「合同(编号)」。

* * @param contractNo 合同编号 * @param contractType 合同类型 * @param supplierShortName 供方简称 * @param customerShortName 客户简称 * @param userContractName 用户填写的合同名称(如果为空则自动生成) * @return 合同名称 */ private String generateContractName(String contractNo, String contractType, String supplierShortName, String customerShortName, String userContractName) { if (StringUtils.isBlank(userContractName)) { // 用户未填写合同名称,根据类型自动生成 if ("sale".equals(contractType) || "framework".equals(contractType)) { // 销售合同、框架合同:供方简称-客户简称(合同编号) return supplierShortName + "-" + customerShortName + "(" + contractNo + ")"; } else if ("purchase".equals(contractType)) { // 采购合同:客户简称-供方简称(合同编号) return customerShortName + "-" + supplierShortName + "(" + contractNo + ")"; } else { // 其他类型:使用合同编号作为名称 return "合同(" + contractNo + ")"; } } else { // 用户手工填写了合同名称,自动补充合同编号 // 先移除旧的合同编号(如果存在) String cleanedName = userContractName.replaceAll("([^)]+)$", ""); // 检查是否已经包含新的合同编号,避免重复添加 if (!cleanedName.contains("(" + contractNo + ")")) { return cleanedName + "(" + contractNo + ")"; } return cleanedName; } } /** * 生成合同编号 * 格式:{供方编码}/X-JT + yyyyMMdd + 3位流水号 * 示例:SX/X-JT20250926002 * * @param supplierId 供方主体ID,如果为null则使用默认编码"SX" * @param signDate 签订日期,如果为null则使用当前日期 */ @Override public String generateContractNo(Long supplierId, LocalDate signDate) { // 获取供方编码,如果没有则使用默认值"SX" String supplierCode = "SX"; if (supplierId != null) { try { CrmSupplierInfoVo supplierInfo = supplierInfoService.queryById(supplierId); if (supplierInfo != null && StringUtils.isNotBlank(supplierInfo.getSupplierCode())) { supplierCode = supplierInfo.getSupplierCode(); } } catch (Exception e) { log.warn("查询供方信息失败,使用默认编码,供方ID: {}", supplierId, e); } } String prefix = supplierCode + "/X-JT"; // 使用签订日期,如果为null则使用当前日期 LocalDate dateToUse = signDate != null ? signDate : LocalDate.now(); String date = dateToUse.format(DateTimeFormatter.ofPattern("yyyyMMdd")); // 查询该签订日期最大流水号(使用动态前缀) LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); wrapper.likeRight(CrmContract::getContractNo, prefix + date); wrapper.orderByDesc(CrmContract::getContractNo); wrapper.last("LIMIT 1"); CrmContract lastContract = baseMapper.selectOne(wrapper); int serialNum = 1; if (lastContract != null && lastContract.getContractNo() != null) { String lastNo = lastContract.getContractNo(); // 完整编号格式:{供方编码}/X-JT20250926002 // 前缀长度可能不同,需要动态计算 int prefixLength = prefix.length(); int dateLength = 8; // yyyyMMdd int totalPrefixLength = prefixLength + dateLength; if (lastNo.length() >= totalPrefixLength + 3) { try { String serialStr = lastNo.substring(totalPrefixLength); serialNum = Integer.parseInt(serialStr) + 1; } catch (NumberFormatException e) { log.warn("解析合同编号流水号失败,使用默认值1,合同编号: {}", lastNo); serialNum = 1; } } } return String.format("%s%s%03d", prefix, date, serialNum); } @Override @Transactional(rollbackFor = Exception.class) public Boolean updateReceivedAmount(Long contractId) { // 查询该合同的所有回款记录 LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.eq(CrmPayment::getContractId, contractId); lqw.eq(CrmPayment::getDelFlag, 0); List payments = paymentMapper.selectList(lqw); // 计算已回款金额 BigDecimal receivedAmount = payments.stream() .map(CrmPayment::getAmount) .filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add); // 查询合同 CrmContract contract = baseMapper.selectById(contractId); if (contract == null) { log.error("合同不存在,无法更新回款金额,合同ID: {}", contractId); return false; } // 更新合同回款金额 CrmContract update = new CrmContract(); update.setId(contractId); update.setReceivedAmount(receivedAmount); update.setUnreceivedAmount(contract.getAmount().subtract(receivedAmount)); boolean flag = baseMapper.updateById(update) > 0; if (flag) { log.info("更新合同回款金额成功,合同ID: {}, 已回款: {}", contractId, receivedAmount); } return flag; } @Override @Transactional(rollbackFor = Exception.class) public Long renewContract(Long parentContractId, CrmContractBo bo) { // 1. 查询原合同 CrmContract parentContract = baseMapper.selectById(parentContractId); if (parentContract == null) { throw new ServiceException("原合同不存在"); } // 2. 创建续签合同 CrmContract newContract = new CrmContract(); // 从原合同复制基础信息 newContract.setCustomerId(parentContract.getCustomerId()); newContract.setCustomerName(parentContract.getCustomerName()); newContract.setContactId(parentContract.getContactId()); newContract.setContactName(parentContract.getContactName()); newContract.setContactMobile(parentContract.getContactMobile()); newContract.setContractType(parentContract.getContractType()); newContract.setOwnerUserId(parentContract.getOwnerUserId()); newContract.setOwnerUserName(parentContract.getOwnerUserName()); // 复制供方主体ID newContract.setSupplierId(bo.getSupplierId() != null ? bo.getSupplierId() : parentContract.getSupplierId()); // 设置续签相关字段 newContract.setContractNature("renewal"); newContract.setParentContractId(parentContractId); newContract .setRenewalCount((parentContract.getRenewalCount() == null ? 0 : parentContract.getRenewalCount()) + 1); // 使用传入的新信息覆盖 if (StringUtils.isNotBlank(bo.getContractName())) { newContract.setContractName(bo.getContractName()); } else { newContract.setContractName(parentContract.getContractName() + "-续签" + newContract.getRenewalCount()); } newContract.setAmount(bo.getAmount() != null ? bo.getAmount() : parentContract.getAmount()); newContract.setCurrency(bo.getCurrency() != null ? bo.getCurrency() : parentContract.getCurrency()); newContract.setTaxRate(bo.getTaxRate() != null ? bo.getTaxRate() : parentContract.getTaxRate()); // 计算含税金额 if (newContract.getTaxRate() != null && newContract.getAmount() != null) { newContract.setAmountWithTax( newContract.getAmount().multiply( BigDecimal.ONE.add( newContract.getTaxRate().divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP)))); } newContract.setSignDate(bo.getSignDate()); newContract.setStartDate(bo.getStartDate()); newContract.setEndDate(bo.getEndDate()); newContract.setContractPeriod(bo.getContractPeriod()); // 复制甲乙方信息 newContract.setPartyA(parentContract.getPartyA()); newContract.setPartyAAddress(parentContract.getPartyAAddress()); newContract.setPartyAContact(parentContract.getPartyAContact()); newContract.setPartyAMobile(parentContract.getPartyAMobile()); newContract.setPartyB(parentContract.getPartyB()); newContract.setPartyBAddress(parentContract.getPartyBAddress()); newContract.setPartyBContact(parentContract.getPartyBContact()); newContract.setPartyBMobile(parentContract.getPartyBMobile()); // 复制合同条款 newContract.setPaymentTerms( bo.getPaymentTerms() != null ? bo.getPaymentTerms() : parentContract.getPaymentTerms()); newContract.setDeliveryTerms( bo.getDeliveryTerms() != null ? bo.getDeliveryTerms() : parentContract.getDeliveryTerms()); newContract.setContractTerms( bo.getContractTerms() != null ? bo.getContractTerms() : parentContract.getContractTerms()); // 生成合同编号(续签合同使用原合同的供方主体) newContract.setContractNo(generateContractNo(newContract.getSupplierId(), newContract.getSignDate())); // 设置默认状态 newContract.setStatus("draft"); newContract.setApprovalStatus(null); // 草稿状态下,审批状态应该为空 newContract.setPerformanceStatus("normal"); newContract.setPerformanceRate(BigDecimal.ZERO); newContract.setReceivedAmount(BigDecimal.ZERO); newContract.setUnreceivedAmount(newContract.getAmount()); newContract.setInvoiceAmount(BigDecimal.ZERO); newContract.setUninvoiceAmount(newContract.getAmount()); // 保存续签合同 boolean flag = baseMapper.insert(newContract) > 0; if (!flag) { throw new ServiceException("创建续签合同失败"); } // 复制产品明细 if (bo.getProducts() != null && !bo.getProducts().isEmpty()) { resolveContractProductIds(bo.getProducts()); // 使用传入的产品 for (CrmContractProductBo product : bo.getProducts()) { product.setContractId(newContract.getId()); // 清空ID,让MyBatis-Plus自动生成新ID,避免主键冲突 product.setId(null); contractProductService.insertByBo(product); if ("framework".equals(parentContract.getContractType())) { savePriceTiersForProduct(product.getId(), newContract.getId(), product.getPriceTiers()); } } } else { // 复制原合同的产品 LambdaQueryWrapper productWrapper = Wrappers.lambdaQuery(); productWrapper.eq(CrmContractProduct::getContractId, parentContractId); List parentProducts = contractProductMapper.selectList(productWrapper); for (CrmContractProduct parentProduct : parentProducts) { CrmContractProduct newProduct = new CrmContractProduct(); newProduct.setContractId(newContract.getId()); newProduct.setProductId(parentProduct.getProductId()); newProduct.setProductName(parentProduct.getProductName()); newProduct.setProductNo(parentProduct.getProductNo()); newProduct.setCategoryId(parentProduct.getCategoryId()); newProduct.setCategoryName(parentProduct.getCategoryName()); newProduct.setModel(parentProduct.getModel()); newProduct.setUnit(parentProduct.getUnit()); newProduct.setQuantity(parentProduct.getQuantity()); newProduct.setPrice(parentProduct.getPrice()); newProduct.setDiscount(parentProduct.getDiscount()); newProduct.setSubtotal(parentProduct.getSubtotal()); newProduct.setDescription(parentProduct.getDescription()); newProduct.setSort(parentProduct.getSort()); contractProductMapper.insert(newProduct); if ("framework".equals(parentContract.getContractType())) { copyPriceTiersToNewProduct(parentProduct.getId(), newContract.getId(), newProduct.getId()); } } } log.info("合同续签成功,原合同ID: {}, 新合同ID: {}, 续签次数: {}", parentContractId, newContract.getId(), newContract.getRenewalCount()); return newContract.getId(); } @Override @Transactional(rollbackFor = Exception.class) public Boolean createFromOpportunity(Long opportunityId, CrmContractBo bo) { // 1. 查询商机 CrmOpportunity opportunity = opportunityMapper.selectById(opportunityId); if (opportunity == null) { throw new ServiceException("商机不存在"); } // 2. 自动填充信息 bo.setOpportunityId(opportunityId); bo.setCustomerId(opportunity.getCustomerId()); if (bo.getAmount() == null) { bo.setAmount(opportunity.getAmount()); } if (bo.getOwnerUserId() == null) { bo.setOwnerUserId(opportunity.getOwnerUserId()); } // 3. 创建合同 boolean flag = insertByBo(bo); // 4. 更新商机状态为"已转合同" if (flag) { CrmOpportunity updateOpp = new CrmOpportunity(); updateOpp.setId(opportunityId); updateOpp.setStage("won"); // 赢单 opportunityMapper.updateById(updateOpp); log.info("从商机创建合同成功,商机ID: {}, 合同ID: {}", opportunityId, bo.getId()); } return flag; } /** * 保存合同前解析产品ID(按名称/规格/分类/单位/税率匹配,未命中自动新增) */ private void resolveContractProductIds(List products) { if (products == null || products.isEmpty()) { return; } for (CrmContractProductBo product : products) { product.setProductId(resolveProductIdForContractItem(product)); } } private Long resolveProductIdForContractItem(CrmContractProductBo contractProductBo) { if (contractProductBo == null) { throw new ServiceException("产品明细不能为空"); } String productName = normalizeText(contractProductBo.getProductName()); if (productName == null) { throw new ServiceException("产品名称不能为空"); } String model = normalizeText(contractProductBo.getModel()); String unit = normalizeText(contractProductBo.getUnit()); BigDecimal taxRateForCompare = normalizeTaxRateForCompare(contractProductBo.getTaxRate()); BigDecimal taxRateForSave = normalizeTaxRateForSave(contractProductBo.getTaxRate()); LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); // 名称/规格/单位:trim + 不区分大小写 + null与空字符串视为相同 lqw.apply("LOWER(TRIM(COALESCE(product_name, ''))) = {0}", toLower(productName)); lqw.apply("LOWER(TRIM(COALESCE(model, ''))) = {0}", toLower(model)); lqw.apply("LOWER(TRIM(COALESCE(unit, ''))) = {0}", toLower(unit)); // 税率:统一保留2位后比较 lqw.apply("ROUND(COALESCE(tax_rate, 0), 2) = {0}", taxRateForCompare); lqw.orderByDesc(CrmProduct::getId); List candidates = productMapper.selectList(lqw); if (!candidates.isEmpty()) { Long categoryId = contractProductBo.getCategoryId(); // 分类为空允许匹配;若有分类,优先取分类一致 if (categoryId != null) { for (CrmProduct candidate : candidates) { if (categoryId.equals(candidate.getCategoryId())) { return candidate.getId(); } } } // 命中多条取最新一条(id最大) return candidates.get(0).getId(); } // 未命中则自动新增产品 CrmProductBo newBo = new CrmProductBo(); newBo.setProductNo(productService.generateAutoProductNo()); newBo.setProductName(productName); newBo.setCategoryId(contractProductBo.getCategoryId()); newBo.setCategoryName(normalizeText(contractProductBo.getCategoryName())); newBo.setModel(model); newBo.setUnit(unit); newBo.setTaxRate(taxRateForSave); // 合同保存自动新增产品时,标准价格默认0 newBo.setPrice(BigDecimal.ZERO); newBo.setStatus("on"); productService.insertByBo(newBo); return newBo.getId(); } private String normalizeText(String value) { if (value == null) { return null; } String trimmed = value.trim(); return trimmed.isEmpty() ? null : trimmed; } private String toLower(String value) { return value == null ? "" : value.toLowerCase(); } private BigDecimal normalizeTaxRateForCompare(BigDecimal value) { BigDecimal taxRate = value == null ? BigDecimal.ZERO : value; return taxRate.setScale(2, RoundingMode.HALF_UP); } /** * 保存税率时尽量保留前端传入形态: * - 传整数则保存整数 * - 传小数则保存小数 */ private BigDecimal normalizeTaxRateForSave(BigDecimal value) { if (value == null) { return BigDecimal.ZERO; } BigDecimal normalized = value.stripTrailingZeros(); if (normalized.scale() <= 0) { return normalized.setScale(0, RoundingMode.UNNECESSARY); } return normalized; } private void validateContractTypeAndProductsPresence(CrmContractBo bo) { if (bo == null || StringUtils.isBlank(bo.getContractType())) { return; } String contractType = bo.getContractType(); if ("sale".equals(contractType) || "purchase".equals(contractType) || "framework".equals(contractType)) { if (bo.getProducts() == null || bo.getProducts().isEmpty()) { throw new ServiceException("销售合同、采购合同或框架合同必须至少添加一个产品"); } } } private void normalizeFrameworkContractData(CrmContractBo bo) { if (bo == null || !"framework".equals(bo.getContractType())) { return; } bo.setAmount(BigDecimal.ZERO); bo.setAmountWithTax(BigDecimal.ZERO); if (bo.getProducts() == null) { return; } for (CrmContractProductBo p : bo.getProducts()) { p.setPrice(BigDecimal.ZERO); p.setSubtotal(BigDecimal.ZERO); p.setTaxAmount(BigDecimal.ZERO); } } private void validateProductPricesByContractType(CrmContractBo bo) { if (bo == null || StringUtils.isBlank(bo.getContractType())) { return; } String contractType = bo.getContractType(); if (!"sale".equals(contractType) && !"purchase".equals(contractType)) { return; } List products = bo.getProducts(); if (products == null) { return; } for (CrmContractProductBo p : products) { if (p.getPrice() == null || p.getPrice().compareTo(new BigDecimal("0.01")) < 0) { throw new ServiceException("销售/采购合同产品的含税单价必须大于0"); } if (p.getSubtotal() == null || p.getSubtotal().compareTo(new BigDecimal("0.01")) < 0) { throw new ServiceException("销售/采购合同产品的含税总价必须大于0"); } } } private void validateSalePurchaseContractAmount(CrmContractBo bo) { if (bo == null || StringUtils.isBlank(bo.getContractType())) { return; } String contractType = bo.getContractType(); if (!"sale".equals(contractType) && !"purchase".equals(contractType)) { return; } if (bo.getAmount() == null || bo.getAmount().compareTo(new BigDecimal("0.01")) < 0) { throw new ServiceException("销售合同或采购合同的合同金额必须大于0"); } } private List parseLetterOssIdsJson(String json) { if (StringUtils.isBlank(json)) { return Collections.emptyList(); } try { List list = JSONUtil.toList(json, String.class); return list.stream().filter(StringUtils::isNotBlank).map(String::trim).toList(); } catch (Exception e) { log.warn("解析 letter_oss_ids JSON 失败: {}", json); return Collections.emptyList(); } } private List parseLetterOssIdsFromEntity(CrmContractProductPriceTier en) { if (StringUtils.isNotBlank(en.getLetterOssIds())) { List parsed = parseLetterOssIdsJson(en.getLetterOssIds()); if (!parsed.isEmpty()) { return new ArrayList<>(parsed); } } if (en.getLetterOssId() != null) { return new ArrayList<>(List.of(en.getLetterOssId().toString())); } return new ArrayList<>(); } private CrmContractProductPriceTierVo convertTierEntityToVo(CrmContractProductPriceTier en) { CrmContractProductPriceTierVo vo = new CrmContractProductPriceTierVo(); vo.setId(en.getId()); vo.setContractId(en.getContractId()); vo.setContractProductId(en.getContractProductId()); vo.setStartDate(en.getStartDate()); vo.setEndDate(en.getEndDate()); vo.setPrice(en.getPrice()); vo.setSort(en.getSort()); vo.setLetterOssIds(parseLetterOssIdsFromEntity(en)); return vo; } private void savePriceTiersForProduct(Long contractProductId, Long contractId, List tiers) { if (tiers == null || tiers.isEmpty()) { return; } int sort = 1; for (CrmContractProductPriceTierBo t : tiers) { if (t.getStartDate() == null || t.getEndDate() == null || t.getPrice() == null) { continue; } List ossIds = t.getLetterOssIds(); if (ossIds == null || ossIds.isEmpty()) { continue; } List normalized = ossIds.stream() .filter(StringUtils::isNotBlank) .map(String::trim) .toList(); if (normalized.isEmpty()) { continue; } CrmContractProductPriceTier e = new CrmContractProductPriceTier(); e.setContractId(contractId); e.setContractProductId(contractProductId); e.setStartDate(t.getStartDate()); e.setEndDate(t.getEndDate()); e.setPrice(t.getPrice()); e.setLetterOssIds(JSONUtil.toJsonStr(normalized)); e.setLetterOssId(null); if (t.getSort() == null) { e.setSort(sort); } else { e.setSort(t.getSort()); } sort++; contractProductPriceTierMapper.insert(e); } } private void copyPriceTiersToNewProduct(Long oldContractProductId, Long newContractId, Long newContractProductId) { List rows = contractProductPriceTierMapper.selectList( Wrappers.lambdaQuery(CrmContractProductPriceTier.class) .eq(CrmContractProductPriceTier::getContractProductId, oldContractProductId)); if (rows == null || rows.isEmpty()) { return; } int s = 1; for (CrmContractProductPriceTier row : rows) { CrmContractProductPriceTier n = new CrmContractProductPriceTier(); n.setContractId(newContractId); n.setContractProductId(newContractProductId); n.setStartDate(row.getStartDate()); n.setEndDate(row.getEndDate()); n.setPrice(row.getPrice()); n.setLetterOssIds(row.getLetterOssIds()); n.setLetterOssId(row.getLetterOssId()); n.setSort(row.getSort() != null ? row.getSort() : s); contractProductPriceTierMapper.insert(n); s++; } } private void validateFrameworkContractForSubmitApproval(Long contractId, CrmContract contract) { if (!"framework".equals(contract.getContractType())) { return; } List products = contractProductService.queryListByContractId(contractId); if (products == null || products.isEmpty()) { throw new ServiceException("框架合同必须维护产品明细"); } List tiers = contractProductPriceTierMapper.selectList( Wrappers.lambdaQuery(CrmContractProductPriceTier.class) .eq(CrmContractProductPriceTier::getContractId, contractId)); Map> byProduct = tiers.stream() .collect(Collectors.groupingBy(CrmContractProductPriceTier::getContractProductId)); for (CrmContractProductVo prod : products) { List list = byProduct.get(prod.getId()); if (list == null || list.isEmpty()) { throw new ServiceException("框架合同产品「" + prod.getProductName() + "」至少维护一条调价区间(含调价函)"); } for (CrmContractProductPriceTier t : list) { if (t.getStartDate() == null || t.getEndDate() == null) { throw new ServiceException("框架合同产品「" + prod.getProductName() + "」调价区间日期不能为空"); } if (t.getStartDate().isAfter(t.getEndDate())) { throw new ServiceException("框架合同产品「" + prod.getProductName() + "」调价开始日期不能晚于结束日期"); } if (t.getPrice() == null || t.getPrice().compareTo(BigDecimal.ZERO) <= 0) { throw new ServiceException("框架合同产品「" + prod.getProductName() + "」调价含税单价必须大于0"); } List letterIds = parseLetterOssIdsFromEntity(t); if (letterIds.isEmpty()) { throw new ServiceException("框架合同产品「" + prod.getProductName() + "」每条调价区间须上传调价函附件"); } } } } @Override @Transactional(rollbackFor = Exception.class) public Boolean createFromQuote(Long quoteId, CrmContractBo bo) { // 1. 查询报价单 CrmQuote quote = quoteMapper.selectById(quoteId); if (quote == null) { throw new ServiceException("报价单不存在"); } // 2. 自动填充信息 bo.setQuoteId(quoteId); bo.setCustomerId(quote.getCustomerId()); bo.setOpportunityId(quote.getOpportunityId()); if (bo.getAmount() == null) { bo.setAmount(quote.getTotalAmount()); } if (bo.getOwnerUserId() == null) { bo.setOwnerUserId(quote.getOwnerUserId()); } // 从报价单复制产品明细(如果没有传入) if ((bo.getProducts() == null || bo.getProducts().isEmpty()) && quote.getId() != null) { // TODO: 从报价单产品表复制产品明细 log.info("从报价单复制产品明细,报价单ID: {}", quoteId); } // 3. 创建合同 boolean flag = insertByBo(bo); // 4. 更新报价单状态为"已转合同" if (flag) { CrmQuote updateQuote = new CrmQuote(); updateQuote.setId(quoteId); updateQuote.setStatus("converted"); // 已转化 quoteMapper.updateById(updateQuote); log.info("从报价单创建合同成功,报价单ID: {}, 合同ID: {}", quoteId, bo.getId()); } return flag; } @Override @Transactional(rollbackFor = Exception.class) public Boolean createFromTemplate(Long templateId, CrmContractBo bo) { // 1. 查询模板 CrmContractTemplate template = contractTemplateMapper.selectById(templateId); if (template == null) { throw new ServiceException("合同模板不存在"); } if (template.getIsActive() == null || template.getIsActive() != 1) { throw new ServiceException("该模板已停用,无法使用"); } // 2. 从模板填充信息 if (bo.getContractType() == null) { bo.setContractType(template.getContractType()); } if (bo.getCurrency() == null) { bo.setCurrency(template.getDefaultCurrency()); } if (bo.getTaxRate() == null) { bo.setTaxRate(template.getDefaultTaxRate()); } if (bo.getContractPeriod() == null) { bo.setContractPeriod(template.getDefaultPeriod()); } // 填充合同条款 if (StringUtils.isBlank(bo.getPaymentTerms())) { bo.setPaymentTerms(template.getPaymentTerms()); } if (StringUtils.isBlank(bo.getDeliveryTerms())) { bo.setDeliveryTerms(template.getDeliveryTerms()); } if (StringUtils.isBlank(bo.getContractTerms())) { bo.setContractTerms(template.getContractTerms()); } // 填充乙方信息 if (StringUtils.isBlank(bo.getPartyB())) { bo.setPartyB(template.getPartyB()); } if (StringUtils.isBlank(bo.getPartyBAddress())) { bo.setPartyBAddress(template.getPartyBAddress()); } if (StringUtils.isBlank(bo.getPartyBContact())) { bo.setPartyBContact(template.getPartyBContact()); } if (StringUtils.isBlank(bo.getPartyBMobile())) { bo.setPartyBMobile(template.getPartyBMobile()); } // 3. 创建合同 boolean flag = insertByBo(bo); // 4. 更新模板使用次数 if (flag) { CrmContractTemplate updateTemplate = new CrmContractTemplate(); updateTemplate.setId(templateId); updateTemplate.setUsageCount((template.getUsageCount() == null ? 0 : template.getUsageCount()) + 1); contractTemplateMapper.updateById(updateTemplate); log.info("从模板创建合同成功,模板ID: {}, 合同ID: {}", templateId, bo.getId()); } return flag; } @Override @Transactional(rollbackFor = Exception.class) public Boolean signContract(Long id, LocalDate actualSignDate, String contractFile, String remark) { // 1. 查询合同 CrmContract contract = baseMapper.selectById(id); if (contract == null) { throw new ServiceException("合同不存在"); } // 2. 校验合同状态 if (!"approved".equals(contract.getStatus())) { throw new ServiceException("只有审批通过的合同才能签订"); } // 3. 更新合同信息 CrmContract update = new CrmContract(); update.setId(id); update.setActualSignDate(actualSignDate); update.setContractFile(contractFile); update.setStatus("executing"); // 签订后进入执行中状态 update.setPerformanceStatus("normal"); update.setPerformanceRate(BigDecimal.ZERO); if (StringUtils.isNotBlank(remark)) { update.setRemark(remark); } boolean flag = baseMapper.updateById(update) > 0; if (flag) { log.info("合同签订成功,合同ID: {}, 签订日期: {}", id, actualSignDate); // TODO: 发送通知给负责人和财务 // TODO: 激活回款计划 } return flag; } @Override @Transactional(rollbackFor = Exception.class) public Boolean batchAssignOwner(Collection ids, Long ownerUserId, String remark) { if (ids == null || ids.isEmpty()) { throw new ServiceException("合同ID列表不能为空"); } if (ownerUserId == null) { throw new ServiceException("负责人ID不能为空"); } // 批量更新 int successCount = 0; for (Long id : ids) { CrmContract contract = baseMapper.selectById(id); if (contract == null) { log.warn("合同不存在,跳过,ID: {}", id); continue; } CrmContract update = new CrmContract(); update.setId(id); update.setOwnerUserId(ownerUserId); // 设置负责人姓名 try { String ownerUserName = userService.selectNicknameById(ownerUserId); update.setOwnerUserName(ownerUserName); } catch (Exception e) { log.warn("获取负责人姓名失败,用户ID: {}", ownerUserId, e); update.setOwnerUserName(null); } if (StringUtils.isNotBlank(remark)) { update.setRemark(remark); } if (baseMapper.updateById(update) > 0) { successCount++; log.info("合同分配成功,合同ID: {}, 新负责人ID: {}", id, ownerUserId); } } log.info("批量分配合同负责人完成,成功: {}, 总数: {}", successCount, ids.size()); return successCount > 0; } @Override public String downloadContractPdf(Long id) { // 查询合同 CrmContractVo contract = queryById(id); if (contract == null) { throw new ServiceException("合同不存在"); } // 返回合同文件路径(如果存在) if (StringUtils.isNotBlank(contract.getContractFile())) { return contract.getContractFile(); } throw new ServiceException("合同PDF文件不存在"); } @Override @Transactional(rollbackFor = Exception.class) public Boolean updateInvoiceAmount(Long contractId) { // 查询该合同的所有发票记录 LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.eq(CrmInvoice::getContractId, contractId); lqw.eq(CrmInvoice::getDelFlag, 0); List invoices = invoiceMapper.selectList(lqw); // 计算已开票金额 BigDecimal invoiceAmount = invoices.stream() .map(CrmInvoice::getAmount) .filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add); // 查询合同 CrmContract contract = baseMapper.selectById(contractId); if (contract == null) { log.error("合同不存在,无法更新开票金额,合同ID: {}", contractId); return false; } // 更新合同开票金额 CrmContract update = new CrmContract(); update.setId(contractId); update.setInvoiceAmount(invoiceAmount); update.setUninvoiceAmount(contract.getAmount().subtract(invoiceAmount)); boolean flag = baseMapper.updateById(update) > 0; if (flag) { log.info("更新合同开票金额成功,合同ID: {}, 已开票: {}", contractId, invoiceAmount); } return flag; } }