package org.dromara.crm.service.impl; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.IdUtil; 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.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.CrmQuote; import org.dromara.crm.domain.CrmContract; import org.dromara.crm.domain.bo.CrmContractBo; import org.dromara.crm.domain.bo.CrmContractProductBo; import org.dromara.crm.domain.bo.CrmQuoteBo; import org.dromara.crm.domain.bo.CrmQuoteProductBo; import org.dromara.crm.domain.vo.CrmQuoteProductVo; import org.dromara.crm.domain.vo.CrmQuoteVo; import org.dromara.crm.mapper.CrmContractMapper; import org.dromara.crm.mapper.CrmQuoteMapper; import org.dromara.crm.mapper.CrmCustomerMapper; import org.dromara.crm.domain.CrmCustomer; import org.dromara.crm.service.*; import org.dromara.crm.service.ICrmQuoteApprovalService; import org.dromara.crm.service.ICrmQuotePdfService; import org.dromara.crm.service.ICrmQuoteProductService; import org.dromara.crm.service.ICrmQuoteService; import org.dromara.system.service.ISysUserService; import org.dromara.system.domain.vo.SysUserVo; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** * 报价单Service实现 * * @author 系统管理员 * @date 2025-01-03 */ @Slf4j @RequiredArgsConstructor @Service public class CrmQuoteServiceImpl implements ICrmQuoteService { private final CrmQuoteMapper baseMapper; private final ICrmQuoteProductService quoteProductService; private final ICrmQuoteApprovalService quoteApprovalService; private final ICrmQuotePdfService quotePdfService; private final ICrmQuoteShareLogService quoteShareLogService; private final ICrmContractService contractService; private final CrmContractMapper contractMapper; private final ICrmOpportunityService opportunityService; private final ICrmContactService contactService; private final ISysUserService userService; private final CrmCustomerMapper customerMapper; private final ICrmSupplierInfoService supplierInfoService; private final ICrmProductService productService; /** * 构建查询条件 */ private LambdaQueryWrapper buildQueryWrapper(CrmQuoteBo bo) { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.like(StringUtils.isNotBlank(bo.getQuoteNo()), CrmQuote::getQuoteNo, bo.getQuoteNo()); // 快速检索关键字(报价主题/客户名称 OR 模糊) if (StringUtils.isNotBlank(bo.getKeyword())) { String keyword = bo.getKeyword(); lqw.and(wrapper -> wrapper .like(CrmQuote::getQuoteTitle, keyword) .or() .like(CrmQuote::getCustomerName, keyword)); } else { // 高级筛选:报价主题和客户名称 // 若两个字段都传入且值相同,按 OR 处理;否则各自模糊匹配(AND) if (StringUtils.isNotBlank(bo.getQuoteTitle()) && StringUtils.isNotBlank(bo.getCustomerName()) && bo.getQuoteTitle().equals(bo.getCustomerName())) { String keyword = bo.getQuoteTitle(); lqw.and(wrapper -> wrapper .like(CrmQuote::getQuoteTitle, keyword) .or() .like(CrmQuote::getCustomerName, keyword)); } else { lqw.like(StringUtils.isNotBlank(bo.getQuoteTitle()), CrmQuote::getQuoteTitle, bo.getQuoteTitle()); lqw.like(StringUtils.isNotBlank(bo.getCustomerName()), CrmQuote::getCustomerName, bo.getCustomerName()); } } lqw.eq(bo.getCustomerId() != null, CrmQuote::getCustomerId, bo.getCustomerId()); lqw.eq(bo.getOpportunityId() != null, CrmQuote::getOpportunityId, bo.getOpportunityId()); lqw.eq(StringUtils.isNotBlank(bo.getStatus()), CrmQuote::getStatus, bo.getStatus()); // 排除指定状态(如默认排除已作废的记录) lqw.ne(StringUtils.isNotBlank(bo.getExcludeStatus()), CrmQuote::getStatus, bo.getExcludeStatus()); lqw.eq(StringUtils.isNotBlank(bo.getQuoteType()), CrmQuote::getQuoteType, bo.getQuoteType()); lqw.eq(bo.getOwnerUserId() != null, CrmQuote::getOwnerUserId, bo.getOwnerUserId()); lqw.eq(bo.getIsLatest() != null, CrmQuote::getIsLatest, bo.getIsLatest()); lqw.ge(bo.getParams().get("minAmount") != null, CrmQuote::getFinalAmount, bo.getParams().get("minAmount")); lqw.le(bo.getParams().get("maxAmount") != null, CrmQuote::getFinalAmount, bo.getParams().get("maxAmount")); // 报价日期范围查询 if (bo.getQuoteDateBegin() != null && !bo.getQuoteDateBegin().isEmpty()) { lqw.ge(CrmQuote::getQuoteDate, LocalDate.parse(bo.getQuoteDateBegin())); } if (bo.getQuoteDateEnd() != null && !bo.getQuoteDateEnd().isEmpty()) { lqw.le(CrmQuote::getQuoteDate, LocalDate.parse(bo.getQuoteDateEnd())); } lqw.between(bo.getParams().get("beginTime") != null && bo.getParams().get("endTime") != null, CrmQuote::getCreateTime, bo.getParams().get("beginTime"), bo.getParams().get("endTime")); // 查询某个用户已审批过的报价单 if (bo.getApprovedByUserId() != null) { lqw.inSql(CrmQuote::getId, "SELECT DISTINCT quote_id FROM crm_quote_approval " + "WHERE approver_id = " + bo.getApprovedByUserId() + " AND approval_result IN ('approved', 'rejected') " + " AND del_flag = 0"); } lqw.orderByDesc(CrmQuote::getCreateTime); return lqw; } @Override public TableDataInfo queryPageList(CrmQuoteBo bo, PageQuery pageQuery) { LambdaQueryWrapper lqw = buildQueryWrapper(bo); Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); // 填充canApprove字段(当前用户是否可以审批) Long currentUserId = LoginHelper.getUserId(); for (CrmQuoteVo vo : result.getRecords()) { // 只有待审批状态的报价单才需要检查 if ("submitted".equals(vo.getStatus()) && currentUserId != null) { vo.setCanApprove(quoteApprovalService.canUserApprove(vo.getId(), currentUserId)); } else { vo.setCanApprove(false); } } fillContractLinkedCount(result.getRecords()); return TableDataInfo.build(result); } @Override public List queryList(CrmQuoteBo bo) { LambdaQueryWrapper lqw = buildQueryWrapper(bo); List list = baseMapper.selectVoList(lqw); // 填充canApprove字段(当前用户是否可以审批) Long currentUserId = LoginHelper.getUserId(); for (CrmQuoteVo vo : list) { // 只有待审批状态的报价单才需要检查 if ("submitted".equals(vo.getStatus()) && currentUserId != null) { vo.setCanApprove(quoteApprovalService.canUserApprove(vo.getId(), currentUserId)); } else { vo.setCanApprove(false); } } fillContractLinkedCount(list); return list; } @Override public CrmQuoteVo queryById(Long id) { CrmQuoteVo vo = baseMapper.selectVoById(id); if (vo != null) { // 查询产品列表 vo.setProductList(quoteProductService.queryListByQuoteId(id)); // 填充商机名称 if (vo.getOpportunityId() != null) { try { var opportunityVo = opportunityService.queryById(vo.getOpportunityId()); if (opportunityVo != null) { vo.setOpportunityName(opportunityVo.getOpportunityName()); } } catch (Exception e) { log.warn("查询商机名称失败, opportunityId: {}", vo.getOpportunityId(), e); } } // 自动补全联系人手机和邮箱(如果为空但有联系人ID) if (vo.getContactId() != null && (StringUtils.isBlank(vo.getContactMobile()) || StringUtils.isBlank(vo.getContactEmail()))) { try { var contact = contactService.queryById(vo.getContactId()); if (contact != null) { if (StringUtils.isBlank(vo.getContactMobile())) { vo.setContactMobile(contact.getMobile()); } if (StringUtils.isBlank(vo.getContactEmail())) { vo.setContactEmail(contact.getEmail()); } } } catch (Exception e) { log.warn("查询联系人信息失败, contactId: {}", vo.getContactId(), e); } } // 填充负责人手机号 if (vo.getOwnerUserId() != null && StringUtils.isBlank(vo.getOwnerUserMobile())) { try { SysUserVo user = userService.selectUserById(vo.getOwnerUserId()); if (user != null && StringUtils.isNotBlank(user.getPhonenumber())) { vo.setOwnerUserMobile(user.getPhonenumber()); } } catch (Exception e) { log.warn("查询负责人手机号失败, ownerUserId: {}", vo.getOwnerUserId(), e); } } // 填充报价主体名称 if (vo.getSupplierId() != null && StringUtils.isBlank(vo.getSupplierName())) { try { var supplierInfo = supplierInfoService.queryById(vo.getSupplierId()); if (supplierInfo != null) { vo.setSupplierName(supplierInfo.getSupplierName()); } } catch (Exception e) { log.warn("查询报价主体信息失败, supplierId: {}", vo.getSupplierId(), e); } } } return vo; } @Override @Transactional(rollbackFor = Exception.class) public Boolean insertByBo(CrmQuoteBo bo) { CrmQuote entity = MapstructUtils.convert(bo, CrmQuote.class); // 查询客户信息 CrmCustomer customer = null; if (entity.getCustomerId() != null) { customer = customerMapper.selectById(entity.getCustomerId()); } // 查询供方信息 String supplierShortName = "山峡"; if (entity.getSupplierId() != null) { try { var supplierInfo = supplierInfoService.queryById(entity.getSupplierId()); if (supplierInfo != null && StringUtils.isNotBlank(supplierInfo.getShortName())) { supplierShortName = supplierInfo.getShortName(); } else if (supplierInfo != null && StringUtils.isNotBlank(supplierInfo.getSupplierName())) { supplierShortName = supplierInfo.getSupplierName(); } } catch (Exception e) { log.warn("查询供方信息失败,使用默认简称,供方ID: {}", entity.getSupplierId(), e); } } // 设置默认值(需要在生成编号之前设置报价日期) if (entity.getVersion() == null) { entity.setVersion("V1.0"); } if (entity.getParentId() == null) { entity.setParentId(0L); } if (entity.getIsLatest() == null) { entity.setIsLatest(1); } if (entity.getStatus() == null) { entity.setStatus("draft"); } if (StringUtils.isBlank(entity.getQuoteType())) { entity.setQuoteType("sales"); } if (entity.getValidityDays() == null) { entity.setValidityDays(30); } if (entity.getQuoteDate() == null) { entity.setQuoteDate(LocalDate.now()); } // 生成报价单编号(根据供方编码和报价日期) // 如果报价单号为空或包含预览标记(***),则重新生成 if (StringUtils.isBlank(entity.getQuoteNo()) || (StringUtils.isNotBlank(entity.getQuoteNo()) && entity.getQuoteNo().contains("***"))) { entity.setQuoteNo(generateQuoteNo(entity.getSupplierId(), entity.getQuoteDate())); } // 自动生成报价主题 String quoteNo = entity.getQuoteNo(); String customerShortName = customer != null && StringUtils.isNotBlank(customer.getShortName()) ? customer.getShortName() : ""; if (StringUtils.isBlank(entity.getQuoteTitle())) { // 用户未填写报价主题,根据报价类型自动生成 // 销售:供方简称-客户简称(报价单编号) // 采购:客户简称-供方简称(报价单编号) String quoteTitle; if ("purchase".equals(entity.getQuoteType())) { // 采购:客户简称-供方简称(报价单编号) quoteTitle = customerShortName + "-" + supplierShortName + "(" + quoteNo + ")"; } else { // 销售:供方简称-客户简称(报价单编号) quoteTitle = supplierShortName + "-" + customerShortName + "(" + quoteNo + ")"; } entity.setQuoteTitle(quoteTitle); } if (entity.getOwnerUserId() == null) { entity.setOwnerUserId(LoginHelper.getUserId()); entity.setOwnerUserName(LoginHelper.getNickname()); } // 填充联系人手机和邮箱 if (entity.getContactId() != null) { try { var contact = contactService.queryById(entity.getContactId()); if (contact != null) { entity.setContactMobile(contact.getMobile()); entity.setContactEmail(contact.getEmail()); } } catch (Exception e) { log.warn("查询联系人信息失败, contactId: {}", entity.getContactId(), e); } } // 填充报价主体名称 if (entity.getSupplierId() != null && StringUtils.isBlank(entity.getSupplierName())) { try { var supplierInfo = supplierInfoService.queryById(entity.getSupplierId()); if (supplierInfo != null) { entity.setSupplierName(supplierInfo.getSupplierName()); } } catch (Exception e) { log.warn("查询报价主体信息失败, supplierId: {}", entity.getSupplierId(), e); } } // 计算到期日期 if (entity.getValidityDays() != null && entity.getQuoteDate() != null) { entity.setExpireDate(entity.getQuoteDate().plusDays(entity.getValidityDays())); } // 计算金额 resolveQuoteProductIds(bo.getProducts()); calculateAmount(entity, bo.getProducts()); // 保存报价单 boolean flag = baseMapper.insert(entity) > 0; if (flag) { bo.setId(entity.getId()); // 保存产品明细 if (bo.getProducts() != null && !bo.getProducts().isEmpty()) { quoteProductService.batchInsert(entity.getId(), bo.getProducts()); } } return flag; } @Override @Transactional(rollbackFor = Exception.class) public Boolean updateByBo(CrmQuoteBo bo) { CrmQuote entity = MapstructUtils.convert(bo, CrmQuote.class); // 只有草稿状态才能修改 CrmQuote oldEntity = baseMapper.selectById(bo.getId()); if (oldEntity == null) { throw new ServiceException("报价单不存在"); } if (!"draft".equals(oldEntity.getStatus())) { throw new ServiceException("只有草稿状态的报价单才能修改"); } if (StringUtils.isBlank(entity.getQuoteType())) { entity.setQuoteType(StringUtils.isNotBlank(oldEntity.getQuoteType()) ? oldEntity.getQuoteType() : "sales"); } // 填充联系人手机和邮箱 if (entity.getContactId() != null) { try { var contact = contactService.queryById(entity.getContactId()); if (contact != null) { entity.setContactMobile(contact.getMobile()); entity.setContactEmail(contact.getEmail()); } } catch (Exception e) { log.warn("查询联系人信息失败, contactId: {}", entity.getContactId(), e); } } // 检查客户、供方或报价日期是否发生变化 boolean customerChanged = !java.util.Objects.equals(oldEntity.getCustomerId(), entity.getCustomerId()); boolean supplierChanged = !java.util.Objects.equals(oldEntity.getSupplierId(), entity.getSupplierId()); boolean quoteDateChanged = !java.util.Objects.equals(oldEntity.getQuoteDate(), entity.getQuoteDate()); // 查询客户信息 CrmCustomer customer = null; if (entity.getCustomerId() != null) { customer = customerMapper.selectById(entity.getCustomerId()); } // 查询供方信息 String supplierShortName = "山峡"; if (entity.getSupplierId() != null) { try { var supplierInfo = supplierInfoService.queryById(entity.getSupplierId()); if (supplierInfo != null) { entity.setSupplierName(supplierInfo.getSupplierName()); if (StringUtils.isNotBlank(supplierInfo.getShortName())) { supplierShortName = supplierInfo.getShortName(); } else if (StringUtils.isNotBlank(supplierInfo.getSupplierName())) { supplierShortName = supplierInfo.getSupplierName(); } } } catch (Exception e) { log.warn("查询报价主体信息失败, supplierId: {}", entity.getSupplierId(), e); } } // 如果报价日期发生变化,需要重新生成编号(根据新的报价日期生成) if (quoteDateChanged && entity.getQuoteDate() != null) { entity.setQuoteNo(generateQuoteNo(entity.getSupplierId(), entity.getQuoteDate())); // 报价单号变化了,需要更新报价主题 String customerShortName = ""; if (customer != null && StringUtils.isNotBlank(customer.getShortName())) { customerShortName = customer.getShortName(); } else if (entity.getCustomerName() != null) { customerShortName = entity.getCustomerName(); } else if (oldEntity.getCustomerName() != null) { customerShortName = oldEntity.getCustomerName(); } String currentQuoteNo = entity.getQuoteNo(); // 根据报价类型生成报价主题 // 销售:供方简称-客户简称(报价单编号) // 采购:客户简称-供方简称(报价单编号) String quoteTitle; if ("purchase".equals(entity.getQuoteType())) { // 采购:客户简称-供方简称(报价单编号) quoteTitle = customerShortName + "-" + supplierShortName + "(" + currentQuoteNo + ")"; } else { // 销售:供方简称-客户简称(报价单编号) quoteTitle = supplierShortName + "-" + customerShortName + "(" + currentQuoteNo + ")"; } entity.setQuoteTitle(quoteTitle); } // 如果客户或供方发生变化,更新报价主题和报价单号(只更新编码和简称,不改变日期和序列号) else if (customerChanged || supplierChanged) { String quoteNo = oldEntity.getQuoteNo(); // 保持原报价单号不变 // 如果供方发生变化,只更新报价单号中的供方编码部分,保持日期和序列号不变 if (supplierChanged && StringUtils.isNotBlank(quoteNo)) { // 报价单号格式:{供方编码}DD-YYYYMMDD001 // 提取日期和序列号部分(DD-YYYYMMDD001) String dateAndSeq = null; if (quoteNo.contains("DD-") && quoteNo.length() > 3) { int ddIndex = quoteNo.indexOf("DD-"); if (ddIndex >= 0 && ddIndex + 3 < quoteNo.length()) { dateAndSeq = quoteNo.substring(ddIndex); } } // 获取新的供方编码 String newSupplierCode = "SX"; if (entity.getSupplierId() != null) { try { var supplierInfo = supplierInfoService.queryById(entity.getSupplierId()); if (supplierInfo != null && StringUtils.isNotBlank(supplierInfo.getSupplierCode())) { newSupplierCode = supplierInfo.getSupplierCode(); } } catch (Exception e) { log.warn("查询供方信息失败,使用默认编码,供方ID: {}", entity.getSupplierId(), e); } } // 如果成功提取了日期和序列号,则用新编码替换 if (dateAndSeq != null) { quoteNo = newSupplierCode + dateAndSeq; entity.setQuoteNo(quoteNo); } // 如果提取失败,保持原报价单号不变 } // 更新报价主题(只更新简称部分,报价单号使用当前数据的报价单号,确保与entity.getQuoteNo()保持一致) String customerShortName = customer != null && StringUtils.isNotBlank(customer.getShortName()) ? customer.getShortName() : ""; // 使用entity.getQuoteNo()确保报价主题中的报价单号与当前数据的报价单号保持一致 // 如果供方变化且成功更新了报价单号,entity.getQuoteNo()就是更新后的值 // 如果供方没变化或提取失败,entity.getQuoteNo()就是原值 String currentQuoteNo = entity.getQuoteNo() != null ? entity.getQuoteNo() : oldEntity.getQuoteNo(); // 根据报价类型生成报价主题 // 销售:供方简称-客户简称(报价单编号) // 采购:客户简称-供方简称(报价单编号) String quoteTitle; if ("purchase".equals(entity.getQuoteType())) { // 采购:客户简称-供方简称(报价单编号) quoteTitle = customerShortName + "-" + supplierShortName + "(" + currentQuoteNo + ")"; } else { // 销售:供方简称-客户简称(报价单编号) quoteTitle = supplierShortName + "-" + customerShortName + "(" + currentQuoteNo + ")"; } entity.setQuoteTitle(quoteTitle); } // 计算到期日期 if (entity.getValidityDays() != null && entity.getQuoteDate() != null) { entity.setExpireDate(entity.getQuoteDate().plusDays(entity.getValidityDays())); } // 计算金额 resolveQuoteProductIds(bo.getProducts()); calculateAmount(entity, bo.getProducts()); boolean flag = baseMapper.updateById(entity) > 0; if (flag && bo.getProducts() != null) { // 删除旧产品明细 quoteProductService.deleteByQuoteId(bo.getId()); // 插入新产品明细 quoteProductService.batchInsert(bo.getId(), bo.getProducts()); } return flag; } @Override @Transactional(rollbackFor = Exception.class) public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { if (isValid) { // 校验是否可以删除 for (Long id : ids) { CrmQuote entity = baseMapper.selectById(id); if (entity == null) { continue; } if (!"draft".equals(entity.getStatus())) { throw new ServiceException("只能删除草稿状态的报价单"); } if (entity.getContractId() != null) { throw new ServiceException("已转合同的报价单不能删除"); } } } return baseMapper.deleteByIds(ids) > 0; } @Override @Transactional(rollbackFor = Exception.class) public Boolean submitQuote(Long id, List approverIds, String remark) { CrmQuote entity = baseMapper.selectById(id); if (entity == null) { throw new ServiceException("报价单不存在"); } if (!"draft".equals(entity.getStatus())) { throw new ServiceException("只有草稿状态的报价单才能提交"); } // 更新状态 entity.setStatus("submitted"); baseMapper.updateById(entity); // 创建审批记录 if (approverIds != null && !approverIds.isEmpty()) { quoteApprovalService.createApprovalRecords(id, approverIds); } // 自动生成PDF try { String pdfUrl = quotePdfService.generateQuotePdf(id); entity.setPdfUrl(pdfUrl); entity.setPdfGenerated(1); baseMapper.updateById(entity); } catch (Exception e) { log.warn("报价单提交时自动生成PDF失败: quoteId={}", id, e); } return true; } @Override public Boolean saveTemplate(Long id, String templateContent) { // 检查报价单是否存在 CrmQuote entity = baseMapper.selectById(id); if (entity == null) { throw new ServiceException("报价单不存在"); } log.info("========== 开始保存报价单模板内容 =========="); log.info("quoteId={}", 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("<")); // 使用 LambdaUpdateWrapper 只更新 template_content 字段,避免乐观锁问题 int result = baseMapper.update(null, Wrappers.lambdaUpdate() .eq(CrmQuote::getId, id) .set(CrmQuote::getTemplateContent, templateContent)); log.info("保存报价单模板内容完成: quoteId={}, success={}, affectedRows={}", id, result > 0, result); // 验证保存结果 CrmQuote 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("quoteId={}", id); // 直接查询,确保获取最新数据 CrmQuote 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 public String generatePdf(Long id) { CrmQuote entity = baseMapper.selectById(id); if (entity == null) { throw new ServiceException("报价单不存在"); } // 调用PDF生成服务 String pdfUrl = quotePdfService.generateQuotePdf(id); // 更新PDF信息 entity.setPdfUrl(pdfUrl); entity.setPdfGenerated(1); baseMapper.updateById(entity); return pdfUrl; } @Override public String generateShare(Long id, Integer validityDays, String password) { CrmQuote entity = baseMapper.selectById(id); if (entity == null) { throw new ServiceException("报价单不存在"); } // 生成分享码(8位随机码) String shareCode = IdUtil.fastSimpleUUID().substring(0, 8); // 计算分享链接过期时间 LocalDateTime shareExpireTime = null; if (validityDays != null && validityDays > 0) { shareExpireTime = LocalDateTime.now().plusDays(validityDays); } // 生成分享链接 // TODO: 这里应该从配置文件读取域名 String shareLink = "http://localhost:8080/share/" + shareCode; // 更新分享信息 entity.setShareCode(shareCode); entity.setShareLink(shareLink); entity.setSharePassword(password); // 设置密码(可为空) entity.setShareExpireTime(shareExpireTime); baseMapper.updateById(entity); log.info("生成分享链接成功: quoteId={}, shareCode={}, hasPassword={}, expireTime={}", id, shareCode, (password != null && !password.isEmpty()), shareExpireTime); return shareLink; } @Override public CrmQuoteVo viewByShareCode(String shareCode, String password, String ipAddress, String userAgent) { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.eq(CrmQuote::getShareCode, shareCode); CrmQuote entity = baseMapper.selectOne(lqw); if (entity == null) { throw new ServiceException("分享链接无效"); } // 检查分享链接是否过期 if (entity.getShareExpireTime() != null && LocalDateTime.now().isAfter(entity.getShareExpireTime())) { throw new ServiceException("分享链接已过期"); } // 验证密码(如果设置了密码) if (StringUtils.isNotBlank(entity.getSharePassword())) { if (StringUtils.isBlank(password) || !entity.getSharePassword().equals(password)) { throw new ServiceException("分享密码错误"); } } // 检查报价单是否已过期 if (entity.getExpireDate() != null && LocalDate.now().isAfter(entity.getExpireDate())) { // 更新报价单状态为已过期 if (!"expired".equals(entity.getStatus())) { entity.setStatus("expired"); baseMapper.updateById(entity); } } // 增加查看次数 entity.setViewCount((entity.getViewCount() == null ? 0 : entity.getViewCount()) + 1); entity.setLastViewTime(LocalDateTime.now()); // 更新状态:如果是已提交或已审批,改为已查看 if ("submitted".equals(entity.getStatus()) || "approved".equals(entity.getStatus())) { entity.setStatus("viewed"); } baseMapper.updateById(entity); // 记录访问日志 try { quoteShareLogService.recordVisit(entity.getId(), shareCode, ipAddress, userAgent); } catch (Exception e) { log.warn("记录访问日志失败: quoteId={}, shareCode={}", entity.getId(), shareCode, e); } log.info("通过分享码查看报价单: shareCode={}, quoteId={}, viewCount={}, ip={}", shareCode, entity.getId(), entity.getViewCount(), ipAddress); CrmQuoteVo vo = MapstructUtils.convert(entity, CrmQuoteVo.class); vo.setProductList(quoteProductService.queryListByQuoteId(entity.getId())); return vo; } @Override public Boolean acceptQuote(Long id, String remark) { CrmQuote entity = baseMapper.selectById(id); if (entity == null) { throw new ServiceException("报价单不存在"); } entity.setStatus("accepted"); return baseMapper.updateById(entity) > 0; } @Override public Boolean rejectQuote(Long id, String rejectReason, String remark) { CrmQuote entity = baseMapper.selectById(id); if (entity == null) { throw new ServiceException("报价单不存在"); } entity.setStatus("rejected"); entity.setRejectReason(rejectReason); return baseMapper.updateById(entity) > 0; } @Override @Transactional(rollbackFor = Exception.class) public Long toContract(Long id, String contractNo, String contractTitle, LocalDate signDate, LocalDate startDate, LocalDate endDate) { // 1. 查询报价单信息 CrmQuote quote = baseMapper.selectById(id); if (quote == null) { throw new ServiceException("报价单不存在"); } if (!"accepted".equals(quote.getStatus())) { throw new ServiceException("只有已接受的报价单才能转合同"); } if (quote.getContractId() != null) { throw new ServiceException("该报价单已转为合同,不能重复转换"); } // 2. 查询报价单产品明细 List quoteProducts = quoteProductService.queryListByQuoteId(id); if (quoteProducts == null || quoteProducts.isEmpty()) { throw new ServiceException("报价单没有产品明细,无法转合同"); } // 3. 构建合同Bo对象 CrmContractBo contractBo = new CrmContractBo(); contractBo.setContractNo(contractNo); contractBo.setContractName(contractTitle); contractBo.setCustomerId(quote.getCustomerId()); contractBo.setOpportunityId(quote.getOpportunityId()); contractBo.setQuoteId(quote.getId()); contractBo.setContactId(quote.getContactId()); contractBo.setContractType("sale"); // 默认销售合同 contractBo.setAmount(quote.getFinalAmount()); contractBo.setSignDate(signDate != null ? signDate : LocalDate.now()); contractBo.setStartDate(startDate != null ? startDate : LocalDate.now()); contractBo.setEndDate(endDate); // 可为空 contractBo.setOwnerUserId(quote.getOwnerUserId()); contractBo.setPaymentTerms(quote.getPaymentTerms()); contractBo.setDeliveryTerms(quote.getDeliveryTerms()); contractBo.setRemark("由报价单" + quote.getQuoteNo() + "转换生成"); // 4. 复制产品明细(只设置Bo中存在的字段) List contractProducts = new java.util.ArrayList<>(); for (CrmQuoteProductVo quoteProduct : quoteProducts) { CrmContractProductBo contractProduct = new CrmContractProductBo(); contractProduct.setProductId(quoteProduct.getProductId()); contractProduct.setQuantity(quoteProduct.getQuantity()); contractProduct.setPrice(quoteProduct.getPrice()); contractProduct.setDiscount(quoteProduct.getDiscount()); contractProduct.setDescription(quoteProduct.getDescription()); contractProduct.setSort(quoteProduct.getSort()); contractProduct.setDeliveryStatus("pending"); contractProduct.setDeliveredQuantity(0); contractProducts.add(contractProduct); } contractBo.setProducts(contractProducts); // 5. 调用合同服务创建合同 Boolean success = contractService.insertByBo(contractBo); if (!success) { throw new ServiceException("创建合同失败"); } Long contractId = contractBo.getId(); // 6. 更新报价单状态 quote.setStatus("contract"); quote.setContractId(contractId); baseMapper.updateById(quote); log.info("报价单转合同成功: quoteId={}, quoteNo={}, contractId={}, contractNo={}", id, quote.getQuoteNo(), contractId, contractNo); return contractId; } @Override @Transactional(rollbackFor = Exception.class) public Long createNewVersion(Long id, String version, String remark) { CrmQuote oldEntity = baseMapper.selectById(id); if (oldEntity == null) { throw new ServiceException("报价单不存在"); } // 创建新版本 CrmQuote newEntity = new CrmQuote(); newEntity.setQuoteNo(oldEntity.getQuoteNo()); newEntity.setQuoteTitle(oldEntity.getQuoteTitle()); newEntity.setCustomerId(oldEntity.getCustomerId()); newEntity.setCustomerName(oldEntity.getCustomerName()); newEntity.setOpportunityId(oldEntity.getOpportunityId()); newEntity.setContactId(oldEntity.getContactId()); newEntity.setContactName(oldEntity.getContactName()); newEntity.setContactMobile(oldEntity.getContactMobile()); newEntity.setContactEmail(oldEntity.getContactEmail()); newEntity.setVersion(version); newEntity.setParentId(id); newEntity.setIsLatest(1); newEntity.setStatus("draft"); newEntity.setValidityDays(oldEntity.getValidityDays()); newEntity.setQuoteDate(LocalDate.now()); newEntity.setExpireDate(LocalDate.now().plusDays(oldEntity.getValidityDays())); newEntity.setOwnerUserId(oldEntity.getOwnerUserId()); newEntity.setOwnerUserName(oldEntity.getOwnerUserName()); newEntity.setQuoteType(StringUtils.isNotBlank(oldEntity.getQuoteType()) ? oldEntity.getQuoteType() : "sales"); newEntity.setPaymentTerms(oldEntity.getPaymentTerms()); newEntity.setDeliveryTerms(oldEntity.getDeliveryTerms()); newEntity.setRemark(remark); // 旧版本设为非最新 oldEntity.setIsLatest(0); baseMapper.updateById(oldEntity); // 保存新版本 baseMapper.insert(newEntity); // 复制产品明细 List oldProducts = quoteProductService.queryListByQuoteId(id); if (oldProducts != null && !oldProducts.isEmpty()) { List newProducts = oldProducts.stream() .map(vo -> MapstructUtils.convert(vo, CrmQuoteProductBo.class)) .toList(); quoteProductService.batchInsert(newEntity.getId(), newProducts); } return newEntity.getId(); } @Override public List queryVersions(String quoteNo) { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.eq(CrmQuote::getQuoteNo, quoteNo); lqw.orderByDesc(CrmQuote::getCreateTime); return baseMapper.selectVoList(lqw); } /** * 生成报价单编号 * 格式:{供方编码}DD-YYYYMMDD001(供方编码DD- + 报价日期年月日 + 3位序号) * * @param supplierId 供方主体ID,如果为null则使用默认编码"SX" * @param quoteDate 报价日期,如果为null则使用当前日期 */ private String generateQuoteNo(Long supplierId, LocalDate quoteDate) { // 获取供方编码,如果没有则使用默认值"SX" String supplierCode = "SX"; if (supplierId != null) { try { var supplierInfo = supplierInfoService.queryById(supplierId); if (supplierInfo != null && StringUtils.isNotBlank(supplierInfo.getSupplierCode())) { supplierCode = supplierInfo.getSupplierCode(); } } catch (Exception e) { log.warn("查询供方信息失败,使用默认编码,供方ID: {}", supplierId, e); } } // 使用报价日期生成日期部分,如果报价日期为空则使用当前日期 LocalDate date = quoteDate != null ? quoteDate : LocalDate.now(); String dateStr = DateTimeFormatter.ofPattern("yyyyMMdd").format(date); String prefix = supplierCode + "DD-" + dateStr; // 查询该报价日期下的最大编号(只查询未删除的记录) // 同时需要匹配报价日期,确保序号按报价日期统计 LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); wrapper.likeRight(CrmQuote::getQuoteNo, prefix); wrapper.eq(CrmQuote::getQuoteDate, date); // 按报价日期查询 wrapper.orderByDesc(CrmQuote::getQuoteNo); wrapper.last("LIMIT 1"); CrmQuote lastQuote = baseMapper.selectOne(wrapper); int sequence = 1; if (lastQuote != null && StringUtils.isNotBlank(lastQuote.getQuoteNo())) { String lastNo = lastQuote.getQuoteNo(); // {供方编码}DD-20251017001,前缀长度是{供方编码长度}+5(DD-yyyyMMdd),序号长度是3位 if (lastNo.length() >= prefix.length() + 3) { String seqStr = lastNo.substring(prefix.length()); try { sequence = Integer.parseInt(seqStr) + 1; } catch (NumberFormatException e) { sequence = 1; } } } // 最多尝试10次,避免无限循环 for (int retry = 0; retry < 10; retry++) { String quoteNo = prefix + String.format("%03d", sequence + retry); // 检查编号是否已存在(只检查未删除的记录) Long count = baseMapper.selectCount( Wrappers.lambdaQuery(CrmQuote.class) .eq(CrmQuote::getQuoteNo, quoteNo)); if (count == 0) { return quoteNo; } // 如果编号已存在,继续尝试下一个序号 log.warn("报价单编号 {} 已存在,尝试生成下一个编号(第{}次重试)", quoteNo, retry + 1); } // 如果重试10次都失败,抛出异常 throw new ServiceException("生成报价单编号失败,请稍后重试"); } /** * 填充报价单被合同关联次数 */ private void fillContractLinkedCount(List quoteList) { if (quoteList == null || quoteList.isEmpty()) { return; } Set quoteIds = quoteList.stream() .map(CrmQuoteVo::getId) .filter(java.util.Objects::nonNull) .collect(Collectors.toSet()); if (quoteIds.isEmpty()) { return; } List contracts = contractMapper.selectList( Wrappers.lambdaQuery() .in(CrmContract::getQuoteId, quoteIds) .select(CrmContract::getQuoteId)); Map contractCountMap = contracts.stream() .map(CrmContract::getQuoteId) .filter(java.util.Objects::nonNull) .collect(Collectors.groupingBy(id -> id, Collectors.counting())); for (CrmQuoteVo vo : quoteList) { Long count = contractCountMap.getOrDefault(vo.getId(), 0L); vo.setContractLinkedCount(count.intValue()); } } /** * 计算金额 */ private void calculateAmount(CrmQuote entity, List products) { if (products == null || products.isEmpty()) { entity.setTotalAmount(BigDecimal.ZERO); entity.setDiscountAmount(BigDecimal.ZERO); entity.setFinalAmount(BigDecimal.ZERO); return; } // 计算总金额(未折扣前) BigDecimal totalAmount = BigDecimal.ZERO; for (CrmQuoteProductBo product : products) { BigDecimal subtotal = product.getPrice() .multiply(product.getQuantity()) .multiply(BigDecimal.ONE.subtract( product.getDiscount() == null ? BigDecimal.ZERO : product.getDiscount().divide(BigDecimal.valueOf(100)))); totalAmount = totalAmount.add(subtotal); } entity.setTotalAmount(totalAmount); // 计算整单折扣 BigDecimal discountRate = entity.getDiscountRate() == null ? BigDecimal.ZERO : entity.getDiscountRate(); BigDecimal discountAmount = totalAmount.multiply(discountRate.divide(BigDecimal.valueOf(100))); entity.setDiscountAmount(discountAmount); // 计算最终金额 entity.setFinalAmount(totalAmount.subtract(discountAmount)); } /** * 解析报价产品ID(按名称/规格/分类/单位/税率匹配,未命中自动新增) */ private void resolveQuoteProductIds(List products) { if (products == null || products.isEmpty()) { return; } for (CrmQuoteProductBo product : products) { Long productId = productService.resolveProductIdForQuoteItem(product); product.setProductId(productId); } } @Override public java.util.Map statisticsOverview(Long ownerUserId, String beginTime, String endTime) { java.util.Map result = new java.util.HashMap<>(); LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); if (ownerUserId != null) { lqw.eq(CrmQuote::getOwnerUserId, ownerUserId); } if (StringUtils.isNotBlank(beginTime)) { lqw.ge(CrmQuote::getCreateTime, beginTime); } if (StringUtils.isNotBlank(endTime)) { lqw.le(CrmQuote::getCreateTime, endTime); } // 总数量 Long totalCount = baseMapper.selectCount(lqw); result.put("totalCount", totalCount); // 各状态数量 LambdaQueryWrapper draftLqw = lqw.clone().eq(CrmQuote::getStatus, "draft"); result.put("draftCount", baseMapper.selectCount(draftLqw)); LambdaQueryWrapper submittedLqw = lqw.clone().eq(CrmQuote::getStatus, "submitted"); result.put("submittedCount", baseMapper.selectCount(submittedLqw)); LambdaQueryWrapper acceptedLqw = lqw.clone().eq(CrmQuote::getStatus, "accepted"); Long acceptedCount = baseMapper.selectCount(acceptedLqw); result.put("acceptedCount", acceptedCount); LambdaQueryWrapper rejectedLqw = lqw.clone().eq(CrmQuote::getStatus, "rejected"); result.put("rejectedCount", baseMapper.selectCount(rejectedLqw)); LambdaQueryWrapper expiredLqw = lqw.clone().eq(CrmQuote::getStatus, "expired"); result.put("expiredCount", baseMapper.selectCount(expiredLqw)); // 总金额和已接受金额 List allQuotes = baseMapper.selectList(lqw); BigDecimal totalAmount = allQuotes.stream() .map(CrmQuote::getFinalAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); result.put("totalAmount", totalAmount); List acceptedQuotes = baseMapper.selectList(acceptedLqw); BigDecimal acceptedAmount = acceptedQuotes.stream() .map(CrmQuote::getFinalAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); result.put("acceptedAmount", acceptedAmount); // 接受率 double acceptRate = totalCount > 0 ? (acceptedCount * 100.0 / totalCount) : 0.0; result.put("acceptRate", String.format("%.2f", acceptRate)); // 平均金额 BigDecimal avgAmount = totalCount > 0 ? totalAmount.divide(BigDecimal.valueOf(totalCount), 2, java.math.RoundingMode.HALF_UP) : BigDecimal.ZERO; result.put("avgAmount", avgAmount); return result; } @Override public List> statisticsStatus() { List> result = new java.util.ArrayList<>(); String[] statusArray = { "draft", "submitted", "accepted", "rejected", "expired", "contract" }; String[] statusNameArray = { "草稿", "已提交", "已接受", "已拒绝", "已过期", "已转合同" }; Long total = baseMapper.selectCount(Wrappers.lambdaQuery()); for (int i = 0; i < statusArray.length; i++) { java.util.Map item = new java.util.HashMap<>(); String status = statusArray[i]; String statusName = statusNameArray[i]; LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.eq(CrmQuote::getStatus, status); Long count = baseMapper.selectCount(lqw); List quotes = baseMapper.selectList(lqw); BigDecimal amount = quotes.stream() .map(CrmQuote::getFinalAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); double percent = total > 0 ? (count * 100.0 / total) : 0.0; item.put("status", status); item.put("statusName", statusName); item.put("count", count); item.put("amount", amount); item.put("percent", String.format("%.2f", percent)); result.add(item); } return result; } @Override public List> statisticsTrend(String type, String beginTime, String endTime) { List> result = new java.util.ArrayList<>(); // 1. 构建查询条件 LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); if (StringUtils.isNotBlank(beginTime)) { lqw.ge(CrmQuote::getCreateTime, beginTime); } if (StringUtils.isNotBlank(endTime)) { lqw.le(CrmQuote::getCreateTime, endTime); } // 2. 查询所有数据 List allQuotes = baseMapper.selectList(lqw); if (allQuotes.isEmpty()) { return result; } // 3. 按时间类型分组统计 java.util.Map> groupedQuotes = new java.util.LinkedHashMap<>(); for (CrmQuote quote : allQuotes) { // createTime继承自BaseEntity,是Date类型,需要转换为LocalDateTime java.util.Date createTimeDate = quote.getCreateTime(); LocalDateTime createTime; if (createTimeDate != null) { createTime = DateUtil.toLocalDateTime(createTimeDate); } else if (quote.getQuoteDate() != null) { // 如果createTime为空,使用quoteDate createTime = quote.getQuoteDate().atStartOfDay(); } else { createTime = LocalDateTime.now(); } String period = getPeriodKey(createTime, type); groupedQuotes.computeIfAbsent(period, k -> new java.util.ArrayList<>()).add(quote); } // 4. 计算每个周期的统计数据 for (java.util.Map.Entry> entry : groupedQuotes.entrySet()) { String period = entry.getKey(); List periodQuotes = entry.getValue(); java.util.Map item = new java.util.HashMap<>(); item.put("period", period); // 总数量 item.put("totalCount", periodQuotes.size()); // 已接受数量 long acceptedCount = periodQuotes.stream() .filter(q -> "accepted".equals(q.getStatus()) || "contract".equals(q.getStatus())) .count(); item.put("acceptedCount", acceptedCount); // 总金额 BigDecimal totalAmount = periodQuotes.stream() .map(CrmQuote::getFinalAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); item.put("totalAmount", totalAmount); // 已接受金额 BigDecimal acceptedAmount = periodQuotes.stream() .filter(q -> "accepted".equals(q.getStatus()) || "contract".equals(q.getStatus())) .map(CrmQuote::getFinalAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); item.put("acceptedAmount", acceptedAmount); // 接受率 double acceptRate = periodQuotes.size() > 0 ? (acceptedCount * 100.0 / periodQuotes.size()) : 0.0; item.put("acceptRate", String.format("%.2f", acceptRate)); result.add(item); } log.info("报价单趋势分析: type={}, periods={}, totalRecords={}", type, result.size(), allQuotes.size()); return result; } /** * 根据时间类型获取周期标识 */ private String getPeriodKey(LocalDateTime dateTime, String type) { if (dateTime == null) { return "未知"; } DateTimeFormatter formatter; switch (type.toLowerCase()) { case "year": formatter = DateTimeFormatter.ofPattern("yyyy年"); break; case "quarter": int quarter = (dateTime.getMonthValue() - 1) / 3 + 1; return dateTime.getYear() + "年Q" + quarter; case "month": default: formatter = DateTimeFormatter.ofPattern("yyyy年MM月"); break; } return dateTime.format(formatter); } @Override @Transactional(rollbackFor = Exception.class) public Boolean withdrawApproval(Long id, String remark) { CrmQuote entity = baseMapper.selectById(id); if (entity == null) { throw new ServiceException("报价单不存在"); } // 只能撤回已提交未审批的报价单 if (!"submitted".equals(entity.getStatus())) { throw new ServiceException("只能撤回已提交的报价单"); } // 检查是否为创建人 if (!entity.getCreateBy().equals(LoginHelper.getUserId())) { throw new ServiceException("只有创建人才能撤回审批"); } // 更新状态为草稿 entity.setStatus("draft"); boolean flag = baseMapper.updateById(entity) > 0; if (flag) { // 删除所有pending状态的审批记录 quoteApprovalService.deletePendingApprovals(id); log.info("撤回审批成功: quoteId={}, quoteNo={}, remark={}", id, entity.getQuoteNo(), remark); } return flag; } @Override @Transactional(rollbackFor = Exception.class) public Boolean invalidQuote(Long id, String remark) { CrmQuote entity = baseMapper.selectById(id); if (entity == null) { throw new ServiceException("报价单不存在"); } // 任何状态都可以作废,但已作废的不能重复作废 if ("invalid".equals(entity.getStatus())) { throw new ServiceException("报价单已作废,不能重复作废"); } // 更新状态为已作废 entity.setStatus("invalid"); if (StringUtils.isNotBlank(remark)) { String currentRemark = entity.getRemark(); if (StringUtils.isNotBlank(currentRemark)) { entity.setRemark(currentRemark + "\n【作废原因】" + remark); } else { entity.setRemark("【作废原因】" + remark); } } boolean flag = baseMapper.updateById(entity) > 0; if (flag) { log.info("作废报价单成功: quoteId={}, quoteNo={}, remark={}", id, entity.getQuoteNo(), remark); } return flag; } }