Files
shanxia2.0/docs/crm-migration/reference/java/quote/service/impl/CrmQuoteServiceImpl.java
T
2026-06-01 08:21:21 +08:00

1330 lines
56 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<CrmQuote> buildQueryWrapper(CrmQuoteBo bo) {
LambdaQueryWrapper<CrmQuote> 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<CrmQuoteVo> queryPageList(CrmQuoteBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<CrmQuote> lqw = buildQueryWrapper(bo);
Page<CrmQuoteVo> 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<CrmQuoteVo> queryList(CrmQuoteBo bo) {
LambdaQueryWrapper<CrmQuote> lqw = buildQueryWrapper(bo);
List<CrmQuoteVo> 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<Long> 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<Long> 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.<CrmQuote>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<CrmQuote> 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<CrmQuoteProductVo> 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<CrmContractProductBo> 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<CrmQuoteProductVo> oldProducts = quoteProductService.queryListByQuoteId(id);
if (oldProducts != null && !oldProducts.isEmpty()) {
List<CrmQuoteProductBo> newProducts = oldProducts.stream()
.map(vo -> MapstructUtils.convert(vo, CrmQuoteProductBo.class))
.toList();
quoteProductService.batchInsert(newEntity.getId(), newProducts);
}
return newEntity.getId();
}
@Override
public List<CrmQuoteVo> queryVersions(String quoteNo) {
LambdaQueryWrapper<CrmQuote> 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<CrmQuote> 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<CrmQuoteVo> quoteList) {
if (quoteList == null || quoteList.isEmpty()) {
return;
}
Set<Long> quoteIds = quoteList.stream()
.map(CrmQuoteVo::getId)
.filter(java.util.Objects::nonNull)
.collect(Collectors.toSet());
if (quoteIds.isEmpty()) {
return;
}
List<CrmContract> contracts = contractMapper.selectList(
Wrappers.<CrmContract>lambdaQuery()
.in(CrmContract::getQuoteId, quoteIds)
.select(CrmContract::getQuoteId));
Map<Long, Long> 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<CrmQuoteProductBo> 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<CrmQuoteProductBo> products) {
if (products == null || products.isEmpty()) {
return;
}
for (CrmQuoteProductBo product : products) {
Long productId = productService.resolveProductIdForQuoteItem(product);
product.setProductId(productId);
}
}
@Override
public java.util.Map<String, Object> statisticsOverview(Long ownerUserId, String beginTime, String endTime) {
java.util.Map<String, Object> result = new java.util.HashMap<>();
LambdaQueryWrapper<CrmQuote> 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<CrmQuote> draftLqw = lqw.clone().eq(CrmQuote::getStatus, "draft");
result.put("draftCount", baseMapper.selectCount(draftLqw));
LambdaQueryWrapper<CrmQuote> submittedLqw = lqw.clone().eq(CrmQuote::getStatus, "submitted");
result.put("submittedCount", baseMapper.selectCount(submittedLqw));
LambdaQueryWrapper<CrmQuote> acceptedLqw = lqw.clone().eq(CrmQuote::getStatus, "accepted");
Long acceptedCount = baseMapper.selectCount(acceptedLqw);
result.put("acceptedCount", acceptedCount);
LambdaQueryWrapper<CrmQuote> rejectedLqw = lqw.clone().eq(CrmQuote::getStatus, "rejected");
result.put("rejectedCount", baseMapper.selectCount(rejectedLqw));
LambdaQueryWrapper<CrmQuote> expiredLqw = lqw.clone().eq(CrmQuote::getStatus, "expired");
result.put("expiredCount", baseMapper.selectCount(expiredLqw));
// 总金额和已接受金额
List<CrmQuote> allQuotes = baseMapper.selectList(lqw);
BigDecimal totalAmount = allQuotes.stream()
.map(CrmQuote::getFinalAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
result.put("totalAmount", totalAmount);
List<CrmQuote> 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<java.util.Map<String, Object>> statisticsStatus() {
List<java.util.Map<String, Object>> 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<String, Object> item = new java.util.HashMap<>();
String status = statusArray[i];
String statusName = statusNameArray[i];
LambdaQueryWrapper<CrmQuote> lqw = Wrappers.lambdaQuery();
lqw.eq(CrmQuote::getStatus, status);
Long count = baseMapper.selectCount(lqw);
List<CrmQuote> 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<java.util.Map<String, Object>> statisticsTrend(String type, String beginTime, String endTime) {
List<java.util.Map<String, Object>> result = new java.util.ArrayList<>();
// 1. 构建查询条件
LambdaQueryWrapper<CrmQuote> lqw = Wrappers.lambdaQuery();
if (StringUtils.isNotBlank(beginTime)) {
lqw.ge(CrmQuote::getCreateTime, beginTime);
}
if (StringUtils.isNotBlank(endTime)) {
lqw.le(CrmQuote::getCreateTime, endTime);
}
// 2. 查询所有数据
List<CrmQuote> allQuotes = baseMapper.selectList(lqw);
if (allQuotes.isEmpty()) {
return result;
}
// 3. 按时间类型分组统计
java.util.Map<String, List<CrmQuote>> 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<String, List<CrmQuote>> entry : groupedQuotes.entrySet()) {
String period = entry.getKey();
List<CrmQuote> periodQuotes = entry.getValue();
java.util.Map<String, Object> 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;
}
}