1330 lines
56 KiB
Java
1330 lines
56 KiB
Java
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;
|
||
}
|
||
|
||
}
|