2015 lines
86 KiB
Java
2015 lines
86 KiB
Java
package org.dromara.crm.service.impl;
|
||
|
||
import cn.hutool.core.date.DateUtil;
|
||
import cn.hutool.json.JSONArray;
|
||
import cn.hutool.json.JSONObject;
|
||
import cn.hutool.json.JSONUtil;
|
||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||
import lombok.RequiredArgsConstructor;
|
||
import lombok.extern.slf4j.Slf4j;
|
||
import org.dromara.common.core.exception.ServiceException;
|
||
import org.dromara.common.core.service.UserService;
|
||
import org.dromara.common.core.utils.MapstructUtils;
|
||
import org.dromara.common.core.utils.StringUtils;
|
||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||
import org.dromara.common.satoken.utils.LoginHelper;
|
||
import org.dromara.crm.domain.*;
|
||
import org.dromara.crm.domain.bo.CrmContractBo;
|
||
import org.dromara.crm.domain.bo.CrmContractProductBo;
|
||
import org.dromara.crm.domain.bo.CrmContractProductPriceTierBo;
|
||
import org.dromara.crm.domain.bo.CrmProductBo;
|
||
import org.dromara.crm.domain.vo.*;
|
||
import org.dromara.crm.mapper.*;
|
||
import org.dromara.crm.service.ICrmContractApprovalService;
|
||
import org.dromara.crm.service.ICrmContractProductService;
|
||
import org.dromara.crm.service.ICrmContractService;
|
||
import org.dromara.crm.service.ICrmPaymentPlanService;
|
||
import org.dromara.crm.service.ICrmProductService;
|
||
import org.dromara.crm.service.ICrmSupplierInfoService;
|
||
import org.springframework.stereotype.Service;
|
||
import org.springframework.transaction.annotation.Transactional;
|
||
|
||
import java.math.BigDecimal;
|
||
import java.math.RoundingMode;
|
||
import java.time.LocalDate;
|
||
import java.time.LocalDateTime;
|
||
import java.time.format.DateTimeFormatter;
|
||
import java.util.*;
|
||
import java.util.stream.Collectors;
|
||
|
||
/**
|
||
* 合同管理Service实现
|
||
*
|
||
* @author AI Assistant
|
||
* @date 2025-10-04
|
||
*/
|
||
@Slf4j
|
||
@RequiredArgsConstructor
|
||
@Service
|
||
public class CrmContractServiceImpl implements ICrmContractService {
|
||
|
||
private final CrmContractMapper baseMapper;
|
||
private final ICrmContractProductService contractProductService;
|
||
private final CrmContractApprovalMapper approvalMapper;
|
||
private final ICrmContractApprovalService contractApprovalService;
|
||
private final ICrmPaymentPlanService paymentPlanService;
|
||
private final CrmPaymentMapper paymentMapper;
|
||
private final CrmOpportunityMapper opportunityMapper;
|
||
private final CrmQuoteMapper quoteMapper;
|
||
private final CrmContractProductMapper contractProductMapper;
|
||
private final CrmContractTemplateMapper contractTemplateMapper;
|
||
private final CrmInvoiceMapper invoiceMapper;
|
||
private final CrmCustomerMapper customerMapper;
|
||
private final CrmContactMapper contactMapper;
|
||
private final UserService userService;
|
||
private final ICrmSupplierInfoService supplierInfoService;
|
||
private final ICrmProductService productService;
|
||
private final CrmProductMapper productMapper;
|
||
private final CrmContractProductPriceTierMapper contractProductPriceTierMapper;
|
||
|
||
/**
|
||
* 构建查询条件
|
||
*/
|
||
private LambdaQueryWrapper<CrmContract> buildQueryWrapper(CrmContractBo bo) {
|
||
LambdaQueryWrapper<CrmContract> lqw = Wrappers.lambdaQuery();
|
||
|
||
// 合同编号、合同名称和客户名称的OR查询
|
||
// 如果三个字段都有值且相同,则认为是联合搜索(OR关系)
|
||
if (StringUtils.isNotBlank(bo.getContractNo()) && StringUtils.isNotBlank(bo.getContractName())
|
||
&& StringUtils.isNotBlank(bo.getCustomerName())
|
||
&& bo.getContractNo().equals(bo.getContractName())
|
||
&& bo.getContractName().equals(bo.getCustomerName())) {
|
||
// 合同编号、合同名称或客户名称包含关键字
|
||
String keyword = bo.getContractNo();
|
||
lqw.and(wrapper -> wrapper
|
||
.like(CrmContract::getContractNo, keyword)
|
||
.or()
|
||
.like(CrmContract::getContractName, keyword)
|
||
.or()
|
||
.like(CrmContract::getCustomerName, keyword));
|
||
} else {
|
||
// 分别查询
|
||
lqw.like(StringUtils.isNotBlank(bo.getContractNo()), CrmContract::getContractNo, bo.getContractNo());
|
||
lqw.like(StringUtils.isNotBlank(bo.getContractName()), CrmContract::getContractName, bo.getContractName());
|
||
lqw.like(StringUtils.isNotBlank(bo.getCustomerName()), CrmContract::getCustomerName, bo.getCustomerName());
|
||
}
|
||
|
||
lqw.eq(bo.getCustomerId() != null, CrmContract::getCustomerId, bo.getCustomerId());
|
||
lqw.eq(bo.getOpportunityId() != null, CrmContract::getOpportunityId, bo.getOpportunityId());
|
||
lqw.eq(bo.getQuoteId() != null, CrmContract::getQuoteId, bo.getQuoteId());
|
||
lqw.eq(StringUtils.isNotBlank(bo.getContractType()), CrmContract::getContractType, bo.getContractType());
|
||
lqw.eq(StringUtils.isNotBlank(bo.getStatus()), CrmContract::getStatus, bo.getStatus());
|
||
// 排除指定状态(如默认排除已作废的记录)
|
||
lqw.ne(StringUtils.isNotBlank(bo.getExcludeStatus()), CrmContract::getStatus, bo.getExcludeStatus());
|
||
lqw.eq(StringUtils.isNotBlank(bo.getApprovalStatus()), CrmContract::getApprovalStatus, bo.getApprovalStatus());
|
||
lqw.eq(bo.getOwnerUserId() != null, CrmContract::getOwnerUserId, bo.getOwnerUserId());
|
||
|
||
// 如果指定了待审批人ID,查询该用户待审批的合同
|
||
if (bo.getPendingApprovalUserId() != null) {
|
||
// 使用SQL子查询:查询审批记录中审批人是当前用户且审批结果是pending且isCurrent=1的合同
|
||
lqw.inSql(CrmContract::getId,
|
||
"SELECT DISTINCT contract_id FROM crm_contract_approval " +
|
||
"WHERE approver_id = " + bo.getPendingApprovalUserId() +
|
||
" AND approval_result = 'pending' " +
|
||
" AND is_current = 1 " +
|
||
" AND del_flag = 0");
|
||
}
|
||
|
||
// 如果指定了审批人ID,查询该用户审批过的合同
|
||
if (bo.getApprovedByUserId() != null) {
|
||
// 查询该审批人已审批过的合同ID列表
|
||
LambdaQueryWrapper<CrmContractApproval> approvalLqw = Wrappers.lambdaQuery();
|
||
approvalLqw.eq(CrmContractApproval::getApproverId, bo.getApprovedByUserId());
|
||
approvalLqw.in(CrmContractApproval::getApprovalResult, "approved", "rejected");
|
||
List<CrmContractApproval> approvalList = approvalMapper.selectList(approvalLqw);
|
||
if (!approvalList.isEmpty()) {
|
||
List<Long> contractIds = approvalList.stream()
|
||
.map(CrmContractApproval::getContractId)
|
||
.distinct()
|
||
.collect(java.util.stream.Collectors.toList());
|
||
lqw.in(CrmContract::getId, contractIds);
|
||
} else {
|
||
// 如果没有审批记录,返回空结果
|
||
lqw.eq(CrmContract::getId, -1L);
|
||
}
|
||
}
|
||
|
||
lqw.ge(bo.getParams().get("minAmount") != null, CrmContract::getAmount, bo.getParams().get("minAmount"));
|
||
lqw.le(bo.getParams().get("maxAmount") != null, CrmContract::getAmount, bo.getParams().get("maxAmount"));
|
||
// 签订日期范围查询
|
||
if (bo.getSignDateBegin() != null && !bo.getSignDateBegin().isEmpty()) {
|
||
lqw.ge(CrmContract::getSignDate, LocalDate.parse(bo.getSignDateBegin()));
|
||
}
|
||
if (bo.getSignDateEnd() != null && !bo.getSignDateEnd().isEmpty()) {
|
||
lqw.le(CrmContract::getSignDate, LocalDate.parse(bo.getSignDateEnd()));
|
||
}
|
||
lqw.between(bo.getParams().get("beginTime") != null && bo.getParams().get("endTime") != null,
|
||
CrmContract::getCreateTime, bo.getParams().get("beginTime"), bo.getParams().get("endTime"));
|
||
lqw.orderByDesc(CrmContract::getCreateTime);
|
||
return lqw;
|
||
}
|
||
|
||
@Override
|
||
public TableDataInfo<CrmContractVo> queryPageList(CrmContractBo bo, PageQuery pageQuery) {
|
||
LambdaQueryWrapper<CrmContract> lqw = buildQueryWrapper(bo);
|
||
Page<CrmContractVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||
|
||
// 设置每个合同的审批权限
|
||
try {
|
||
Long currentUserId = LoginHelper.getUserId();
|
||
for (CrmContractVo vo : result.getRecords()) {
|
||
vo.setCanApprove(contractApprovalService.canUserApprove(vo.getId(), currentUserId));
|
||
}
|
||
} catch (Exception e) {
|
||
log.warn("获取当前用户审批权限失败: {}", e.getMessage());
|
||
}
|
||
|
||
return TableDataInfo.build(result);
|
||
}
|
||
|
||
@Override
|
||
public List<CrmContractVo> queryList(CrmContractBo bo) {
|
||
LambdaQueryWrapper<CrmContract> lqw = buildQueryWrapper(bo);
|
||
return baseMapper.selectVoList(lqw);
|
||
}
|
||
|
||
@Override
|
||
public CrmContractVo queryById(Long id) {
|
||
CrmContractVo vo = baseMapper.selectVoById(id);
|
||
if (vo != null) {
|
||
// 设置当前用户是否可以审批
|
||
try {
|
||
Long currentUserId = LoginHelper.getUserId();
|
||
vo.setCanApprove(contractApprovalService.canUserApprove(id, currentUserId));
|
||
} catch (Exception e) {
|
||
log.warn("获取当前用户审批权限失败: {}", e.getMessage());
|
||
vo.setCanApprove(false);
|
||
}
|
||
|
||
// 关联查询供方主体名称
|
||
if (vo.getSupplierId() != null) {
|
||
try {
|
||
CrmSupplierInfoVo supplierInfo = supplierInfoService.queryById(vo.getSupplierId());
|
||
if (supplierInfo != null) {
|
||
vo.setSupplierName(supplierInfo.getSupplierName());
|
||
}
|
||
} catch (Exception e) {
|
||
log.warn("获取供方主体信息失败: {}", e.getMessage());
|
||
}
|
||
}
|
||
|
||
// 查询并设置产品明细列表
|
||
try {
|
||
List<CrmContractProductVo> products = contractProductService.queryListByContractId(id);
|
||
if (vo.getProducts() == null || vo.getProducts().isEmpty()) {
|
||
vo.setProducts(products);
|
||
}
|
||
if ("framework".equals(vo.getContractType()) && products != null && !products.isEmpty()) {
|
||
List<CrmContractProductPriceTier> tierEntities = contractProductPriceTierMapper.selectList(
|
||
Wrappers.lambdaQuery(CrmContractProductPriceTier.class)
|
||
.eq(CrmContractProductPriceTier::getContractId, id)
|
||
.orderByAsc(CrmContractProductPriceTier::getContractProductId)
|
||
.orderByAsc(CrmContractProductPriceTier::getSort));
|
||
List<CrmContractProductPriceTierVo> allTiers = new ArrayList<>();
|
||
for (CrmContractProductPriceTier en : tierEntities) {
|
||
allTiers.add(convertTierEntityToVo(en));
|
||
}
|
||
Map<Long, List<CrmContractProductPriceTierVo>> tierMap = new HashMap<>();
|
||
for (CrmContractProductPriceTierVo t : allTiers) {
|
||
tierMap.computeIfAbsent(t.getContractProductId(), k -> new ArrayList<>()).add(t);
|
||
}
|
||
for (CrmContractProductVo p : products) {
|
||
p.setPriceTiers(tierMap.getOrDefault(p.getId(), new ArrayList<>()));
|
||
}
|
||
}
|
||
log.info("查询合同详情成功,合同ID: {}, 产品数量: {}", id, products != null ? products.size() : 0);
|
||
} catch (Exception e) {
|
||
log.error("查询合同产品明细失败,合同ID: {}", id, e);
|
||
}
|
||
}
|
||
return vo;
|
||
}
|
||
|
||
@Override
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public Boolean insertByBo(CrmContractBo bo) {
|
||
validateContractTypeAndProductsPresence(bo);
|
||
normalizeFrameworkContractData(bo);
|
||
validateProductPricesByContractType(bo);
|
||
validateSalePurchaseContractAmount(bo);
|
||
|
||
CrmContract entity = MapstructUtils.convert(bo, CrmContract.class);
|
||
|
||
// 查询供方信息(用于生成合同编号和合同名称)
|
||
String supplierShortName = "山峡"; // 默认简称
|
||
if (entity.getSupplierId() != null) {
|
||
try {
|
||
CrmSupplierInfoVo supplierInfo = supplierInfoService.queryById(entity.getSupplierId());
|
||
if (supplierInfo != null && StringUtils.isNotBlank(supplierInfo.getShortName())) {
|
||
supplierShortName = supplierInfo.getShortName();
|
||
}
|
||
} catch (Exception e) {
|
||
log.warn("查询供方信息失败,使用默认简称,供方ID: {}", entity.getSupplierId(), e);
|
||
}
|
||
}
|
||
|
||
// 生成合同编号
|
||
if (StringUtils.isBlank(entity.getContractNo())) {
|
||
entity.setContractNo(generateContractNo(entity.getSupplierId(), entity.getSignDate()));
|
||
}
|
||
|
||
// 查询客户信息
|
||
CrmCustomer customer = null;
|
||
if (entity.getCustomerId() != null) {
|
||
customer = customerMapper.selectById(entity.getCustomerId());
|
||
if (customer != null) {
|
||
entity.setCustomerName(customer.getCustomerName());
|
||
}
|
||
}
|
||
|
||
String contractType = entity.getContractType();
|
||
|
||
// 自动生成合同名称
|
||
String contractNo = entity.getContractNo();
|
||
String customerShortName = customer != null ? customer.getShortName() : "";
|
||
entity.setContractName(generateContractName(contractNo, contractType, supplierShortName,
|
||
customerShortName, entity.getContractName()));
|
||
|
||
// 查询并设置联系人信息
|
||
if (entity.getContactId() != null) {
|
||
CrmContact contact = contactMapper.selectById(entity.getContactId());
|
||
if (contact != null) {
|
||
entity.setContactName(contact.getContactName());
|
||
entity.setContactMobile(contact.getMobile());
|
||
}
|
||
}
|
||
|
||
// 设置默认值
|
||
if (entity.getStatus() == null) {
|
||
entity.setStatus("draft");
|
||
}
|
||
// 草稿状态下,审批状态应该为空,只有提交审批后才设置为pending
|
||
if ("draft".equals(entity.getStatus())) {
|
||
entity.setApprovalStatus(null);
|
||
}
|
||
if (entity.getOwnerUserId() == null) {
|
||
entity.setOwnerUserId(LoginHelper.getUserId());
|
||
}
|
||
|
||
// 设置负责人姓名
|
||
if (entity.getOwnerUserId() != null) {
|
||
try {
|
||
String ownerUserName = userService.selectNicknameById(entity.getOwnerUserId());
|
||
entity.setOwnerUserName(ownerUserName);
|
||
} catch (Exception e) {
|
||
log.warn("获取负责人姓名失败,用户ID: {}", entity.getOwnerUserId(), e);
|
||
entity.setOwnerUserName(null);
|
||
}
|
||
} else {
|
||
entity.setOwnerUserName(null);
|
||
}
|
||
|
||
if (entity.getReceivedAmount() == null) {
|
||
entity.setReceivedAmount(BigDecimal.ZERO);
|
||
}
|
||
if (entity.getInvoiceAmount() == null) {
|
||
entity.setInvoiceAmount(BigDecimal.ZERO);
|
||
}
|
||
|
||
// 计算未回款金额和未开票金额
|
||
entity.setUnreceivedAmount(entity.getAmount().subtract(entity.getReceivedAmount()));
|
||
entity.setUninvoiceAmount(entity.getAmount().subtract(entity.getInvoiceAmount()));
|
||
|
||
// 保存合同
|
||
resolveContractProductIds(bo.getProducts());
|
||
boolean flag = baseMapper.insert(entity) > 0;
|
||
if (flag) {
|
||
bo.setId(entity.getId());
|
||
|
||
// 保存产品明细
|
||
if (bo.getProducts() != null && !bo.getProducts().isEmpty()) {
|
||
int sort = 1;
|
||
for (CrmContractProductBo product : bo.getProducts()) {
|
||
product.setContractId(entity.getId());
|
||
// 清空ID,让MyBatis-Plus自动生成新ID,避免主键冲突
|
||
product.setId(null);
|
||
// 如果没有设置排序号,则自动设置为循环索引
|
||
if (product.getSort() == null) {
|
||
product.setSort(sort);
|
||
}
|
||
sort++;
|
||
contractProductService.insertByBo(product);
|
||
if ("framework".equals(entity.getContractType())) {
|
||
savePriceTiersForProduct(product.getId(), entity.getId(), product.getPriceTiers());
|
||
}
|
||
}
|
||
}
|
||
|
||
// 生成回款计划
|
||
if (bo.getPaymentPlans() != null && !bo.getPaymentPlans().isEmpty()) {
|
||
try {
|
||
paymentPlanService.batchGenerate(entity.getId(), bo.getPaymentPlans());
|
||
log.info("合同创建成功,已生成回款计划,合同ID: {}, 期数: {}", entity.getId(), bo.getPaymentPlans().size());
|
||
} catch (Exception e) {
|
||
log.error("生成回款计划失败,合同ID: {}", entity.getId(), e);
|
||
// 不影响合同创建,只记录日志
|
||
}
|
||
}
|
||
}
|
||
return flag;
|
||
}
|
||
|
||
@Override
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public Boolean updateByBo(CrmContractBo bo) {
|
||
// 只有草稿状态的合同才能修改
|
||
CrmContract existContract = baseMapper.selectById(bo.getId());
|
||
if (existContract == null) {
|
||
throw new ServiceException("合同不存在");
|
||
}
|
||
if (!"draft".equals(existContract.getStatus())) {
|
||
throw new ServiceException("只有草稿状态的合同才能修改");
|
||
}
|
||
|
||
validateContractTypeAndProductsPresence(bo);
|
||
normalizeFrameworkContractData(bo);
|
||
validateProductPricesByContractType(bo);
|
||
validateSalePurchaseContractAmount(bo);
|
||
|
||
CrmContract entity = MapstructUtils.convert(bo, CrmContract.class);
|
||
|
||
// 草稿状态下,审批状态应该为空
|
||
if ("draft".equals(existContract.getStatus())) {
|
||
entity.setApprovalStatus(null);
|
||
}
|
||
|
||
// 查询并设置客户名称(如果客户ID有变化)
|
||
CrmCustomer customer = null;
|
||
if (entity.getCustomerId() != null && !entity.getCustomerId().equals(existContract.getCustomerId())) {
|
||
customer = customerMapper.selectById(entity.getCustomerId());
|
||
if (customer != null) {
|
||
entity.setCustomerName(customer.getCustomerName());
|
||
}
|
||
} else if (entity.getCustomerId() != null) {
|
||
// 客户ID没变化,但可能需要用于生成合同名称
|
||
customer = customerMapper.selectById(entity.getCustomerId());
|
||
}
|
||
|
||
// 查询并设置联系人信息(如果联系人ID有变化)
|
||
if (entity.getContactId() != null && !entity.getContactId().equals(existContract.getContactId())) {
|
||
CrmContact contact = contactMapper.selectById(entity.getContactId());
|
||
if (contact != null) {
|
||
entity.setContactName(contact.getContactName());
|
||
entity.setContactMobile(contact.getMobile());
|
||
}
|
||
}
|
||
|
||
// 检查签订日期是否变化,如果变化则重新生成合同编号和合同名称
|
||
boolean signDateChanged = entity.getSignDate() != null
|
||
&& !entity.getSignDate().equals(existContract.getSignDate());
|
||
boolean supplierIdChanged = entity.getSupplierId() != null
|
||
&& !entity.getSupplierId().equals(existContract.getSupplierId());
|
||
|
||
if (signDateChanged || supplierIdChanged) {
|
||
// 查询供方信息(用于生成合同编号和合同名称)
|
||
String supplierShortName = "山峡"; // 默认简称
|
||
if (entity.getSupplierId() != null) {
|
||
try {
|
||
CrmSupplierInfoVo supplierInfo = supplierInfoService.queryById(entity.getSupplierId());
|
||
if (supplierInfo != null && StringUtils.isNotBlank(supplierInfo.getShortName())) {
|
||
supplierShortName = supplierInfo.getShortName();
|
||
}
|
||
} catch (Exception e) {
|
||
log.warn("查询供方信息失败,使用默认简称,供方ID: {}", entity.getSupplierId(), e);
|
||
}
|
||
}
|
||
|
||
// 重新生成合同编号
|
||
entity.setContractNo(generateContractNo(entity.getSupplierId(), entity.getSignDate()));
|
||
|
||
// 重新生成合同名称
|
||
String contractNo = entity.getContractNo();
|
||
String customerShortName = customer != null ? customer.getShortName() : "";
|
||
// 判断用户是否修改了合同名称
|
||
// 如果用户修改了合同名称,使用用户的新名称;否则使用旧名称并更新编号
|
||
String userContractName = null;
|
||
if (StringUtils.isNotBlank(entity.getContractName())
|
||
&& !entity.getContractName().equals(existContract.getContractName())) {
|
||
// 用户修改了合同名称,使用用户的新名称(移除可能存在的旧编号)
|
||
userContractName = entity.getContractName().replaceAll("([^)]+)$", "");
|
||
} else {
|
||
// 用户没有修改合同名称,使用旧名称(移除旧的合同编号)
|
||
String oldContractName = existContract.getContractName();
|
||
if (StringUtils.isNotBlank(oldContractName)) {
|
||
userContractName = oldContractName.replaceAll("([^)]+)$", "");
|
||
}
|
||
}
|
||
entity.setContractName(generateContractName(contractNo, entity.getContractType(), supplierShortName,
|
||
customerShortName, userContractName));
|
||
}
|
||
|
||
// 重新计算未回款金额和未开票金额
|
||
if (entity.getAmount() != null) {
|
||
BigDecimal receivedAmount = existContract.getReceivedAmount() != null
|
||
? existContract.getReceivedAmount()
|
||
: BigDecimal.ZERO;
|
||
BigDecimal invoiceAmount = existContract.getInvoiceAmount() != null
|
||
? existContract.getInvoiceAmount()
|
||
: BigDecimal.ZERO;
|
||
entity.setUnreceivedAmount(entity.getAmount().subtract(receivedAmount));
|
||
entity.setUninvoiceAmount(entity.getAmount().subtract(invoiceAmount));
|
||
}
|
||
|
||
boolean flag = baseMapper.updateById(entity) > 0;
|
||
if (flag && bo.getProducts() != null) {
|
||
resolveContractProductIds(bo.getProducts());
|
||
contractProductPriceTierMapper.delete(Wrappers.lambdaQuery(CrmContractProductPriceTier.class)
|
||
.eq(CrmContractProductPriceTier::getContractId, bo.getId()));
|
||
// 更新产品明细(简化处理:先删除再新增)
|
||
contractProductService.deleteByIds(
|
||
contractProductService.queryListByContractId(bo.getId())
|
||
.stream().map(p -> p.getId()).toList());
|
||
int sort = 1;
|
||
for (CrmContractProductBo product : bo.getProducts()) {
|
||
product.setContractId(bo.getId());
|
||
// 清空ID,让MyBatis-Plus自动生成新ID,避免主键冲突
|
||
product.setId(null);
|
||
// 如果没有设置排序号,则自动设置为循环索引
|
||
if (product.getSort() == null) {
|
||
product.setSort(sort);
|
||
}
|
||
sort++;
|
||
contractProductService.insertByBo(product);
|
||
if ("framework".equals(bo.getContractType())) {
|
||
savePriceTiersForProduct(product.getId(), bo.getId(), product.getPriceTiers());
|
||
}
|
||
}
|
||
}
|
||
return flag;
|
||
}
|
||
|
||
@Override
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
||
if (isValid) {
|
||
// 校验:只能删除草稿状态的合同
|
||
for (Long id : ids) {
|
||
CrmContract contract = baseMapper.selectById(id);
|
||
if (contract != null && !"draft".equals(contract.getStatus())) {
|
||
throw new ServiceException("只能删除草稿状态的合同");
|
||
}
|
||
}
|
||
}
|
||
return baseMapper.deleteByIds(ids) > 0;
|
||
}
|
||
|
||
@Override
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public Boolean submitApproval(Long id, List<Long> approverIds, String remark) {
|
||
CrmContract contract = baseMapper.selectById(id);
|
||
if (contract == null) {
|
||
throw new ServiceException("合同不存在");
|
||
}
|
||
if (!"draft".equals(contract.getStatus())) {
|
||
throw new ServiceException("只有草稿状态的合同才能提交审批");
|
||
}
|
||
|
||
validateFrameworkContractForSubmitApproval(id, contract);
|
||
|
||
// 更新合同状态
|
||
CrmContract update = new CrmContract();
|
||
update.setId(id);
|
||
update.setStatus("approving");
|
||
update.setApprovalStatus("pending");
|
||
baseMapper.updateById(update);
|
||
|
||
// 创建审批记录
|
||
if (approverIds != null && !approverIds.isEmpty()) {
|
||
for (int i = 0; i < approverIds.size(); i++) {
|
||
Long approverId = approverIds.get(i);
|
||
|
||
CrmContractApproval approval = new CrmContractApproval();
|
||
approval.setContractId(id);
|
||
approval.setStep(i + 1);
|
||
approval.setStepName("第" + (i + 1) + "步审批");
|
||
approval.setApproverId(approverId);
|
||
|
||
// TODO: 这里应该查询用户信息填充审批人姓名和部门
|
||
// 由于跨模块调用复杂性,暂时在审批时填充
|
||
// 或者可以通过远程API调用System模块的用户服务
|
||
approval.setApproverName("审批人" + approverId); // 临时占位
|
||
approval.setApproverDept("待审批时填充"); // 临时占位
|
||
|
||
approval.setApprovalResult("pending");
|
||
approval.setIsCurrent(i == 0 ? 1 : 0);
|
||
approval.setSort(i + 1);
|
||
approvalMapper.insert(approval);
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
@Override
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public int batchSubmitApproval(List<Long> ids, List<Long> approverIds, String remark) {
|
||
int successCount = 0;
|
||
for (Long id : ids) {
|
||
try {
|
||
submitApproval(id, approverIds, remark);
|
||
successCount++;
|
||
} catch (Exception e) {
|
||
// 记录错误但继续处理其他合同
|
||
log.error("合同ID {} 提交审批失败: {}", id, e.getMessage());
|
||
}
|
||
}
|
||
return successCount;
|
||
}
|
||
|
||
@Override
|
||
public Boolean saveTemplate(Long id, String templateContent, String settlementClauseDrafts) {
|
||
// 检查合同是否存在
|
||
CrmContract entity = baseMapper.selectById(id);
|
||
if (entity == null) {
|
||
throw new ServiceException("合同不存在");
|
||
}
|
||
String finalSettlementClauseDrafts = settlementClauseDrafts;
|
||
if (StringUtils.isBlank(finalSettlementClauseDrafts)) {
|
||
String method = StringUtils.isNotBlank(entity.getSettlementMethod()) ? entity.getSettlementMethod() : "arrive_payment";
|
||
String defaultClause = "advance_payment".equals(method)
|
||
? "款到发货。需方付款后安排发货,需方收到货物后,供方开具13%增值税专用发票。"
|
||
: "货到付款,需方收到供方货物后,供方开具13%增值税专用发票,无异议后,以现汇方式月结。";
|
||
JSONObject drafts = new JSONObject();
|
||
drafts.set(method, defaultClause);
|
||
finalSettlementClauseDrafts = drafts.toString();
|
||
}
|
||
|
||
log.info("========== 开始保存合同模板内容 ==========");
|
||
log.info("contractId={}", id);
|
||
log.info("contentLength={}", templateContent != null ? templateContent.length() : 0);
|
||
log.info("content前200字符={}", templateContent != null && templateContent.length() > 200
|
||
? templateContent.substring(0, 200)
|
||
: templateContent);
|
||
log.info("包含HTML标签={}", templateContent != null && templateContent.contains("<"));
|
||
log.info("第五条草稿长度={}", finalSettlementClauseDrafts != null ? finalSettlementClauseDrafts.length() : 0);
|
||
|
||
// 使用 LambdaUpdateWrapper 只更新模板相关字段,避免乐观锁问题
|
||
int result = baseMapper.update(null,
|
||
Wrappers.<CrmContract>lambdaUpdate()
|
||
.eq(CrmContract::getId, id)
|
||
.set(CrmContract::getTemplateContent, templateContent)
|
||
.set(CrmContract::getSettlementClauseDrafts, finalSettlementClauseDrafts));
|
||
|
||
log.info("保存合同模板内容完成: contractId={}, success={}, affectedRows={}",
|
||
id, result > 0, result);
|
||
|
||
// 验证保存结果
|
||
CrmContract savedEntity = baseMapper.selectById(id);
|
||
if (savedEntity != null && savedEntity.getTemplateContent() != null) {
|
||
log.info("验证保存结果: 数据库中的内容长度={}", savedEntity.getTemplateContent().length());
|
||
log.info("验证保存结果: 数据库中的内容前200字符={}",
|
||
savedEntity.getTemplateContent().length() > 200
|
||
? savedEntity.getTemplateContent().substring(0, 200)
|
||
: savedEntity.getTemplateContent());
|
||
} else {
|
||
log.warn("验证保存结果: 数据库中未找到模板内容!");
|
||
}
|
||
|
||
return result > 0;
|
||
}
|
||
|
||
@Override
|
||
public String getTemplate(Long id) {
|
||
log.info("========== 开始获取合同模板内容 ==========");
|
||
log.info("contractId={}", id);
|
||
|
||
// 直接查询,确保获取最新数据
|
||
CrmContract entity = baseMapper.selectById(id);
|
||
if (entity == null) {
|
||
throw new ServiceException("合同不存在");
|
||
}
|
||
|
||
String templateContent = entity.getTemplateContent();
|
||
log.info("获取合同模板内容完成:");
|
||
log.info("- hasTemplate={}", templateContent != null);
|
||
log.info("- contentLength={}", templateContent != null ? templateContent.length() : 0);
|
||
log.info("- content前200字符={}", templateContent != null && templateContent.length() > 200
|
||
? templateContent.substring(0, 200)
|
||
: templateContent);
|
||
log.info("- 包含HTML标签={}", templateContent != null && templateContent.contains("<"));
|
||
|
||
return templateContent;
|
||
}
|
||
|
||
@Override
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public Boolean withdrawApproval(Long id, String remark) {
|
||
CrmContract contract = baseMapper.selectById(id);
|
||
if (contract == null) {
|
||
throw new ServiceException("合同不存在");
|
||
}
|
||
if (!"approving".equals(contract.getStatus())) {
|
||
throw new ServiceException("只有审批中的合同才能撤回");
|
||
}
|
||
|
||
// 使用 UpdateWrapper 来明确更新 null 值
|
||
com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<CrmContract> updateWrapper = Wrappers
|
||
.lambdaUpdate();
|
||
updateWrapper.eq(CrmContract::getId, id)
|
||
.set(CrmContract::getStatus, "draft")
|
||
.set(CrmContract::getApprovalStatus, null); // 明确设置为 null
|
||
baseMapper.update(null, updateWrapper);
|
||
|
||
// 删除待审批的审批记录
|
||
LambdaQueryWrapper<CrmContractApproval> wrapper = Wrappers.lambdaQuery();
|
||
wrapper.eq(CrmContractApproval::getContractId, id);
|
||
wrapper.eq(CrmContractApproval::getApprovalResult, "pending");
|
||
approvalMapper.delete(wrapper);
|
||
|
||
log.info("撤回审批成功,合同ID: {}, 撤回原因: {}", id, remark);
|
||
return true;
|
||
}
|
||
|
||
@Override
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public Boolean startExecute(Long id, String remark) {
|
||
CrmContract contract = baseMapper.selectById(id);
|
||
if (contract == null) {
|
||
throw new ServiceException("合同不存在");
|
||
}
|
||
if (!"approved".equals(contract.getStatus())) {
|
||
throw new ServiceException("只有已审批的合同才能开始执行");
|
||
}
|
||
|
||
CrmContract update = new CrmContract();
|
||
update.setId(id);
|
||
update.setStatus("executing");
|
||
return baseMapper.updateById(update) > 0;
|
||
}
|
||
|
||
@Override
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public Boolean finishContract(Long id, String remark) {
|
||
CrmContract contract = baseMapper.selectById(id);
|
||
if (contract == null) {
|
||
throw new ServiceException("合同不存在");
|
||
}
|
||
if (!"executing".equals(contract.getStatus())) {
|
||
throw new ServiceException("只有执行中的合同才能完成");
|
||
}
|
||
|
||
CrmContract update = new CrmContract();
|
||
update.setId(id);
|
||
update.setStatus("finished");
|
||
if (StringUtils.isNotBlank(remark)) {
|
||
update.setRemark(remark);
|
||
}
|
||
return baseMapper.updateById(update) > 0;
|
||
}
|
||
|
||
@Override
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public Boolean invalidContract(Long id, String invalidReason, String remark) {
|
||
if (StringUtils.isBlank(invalidReason)) {
|
||
throw new ServiceException("作废原因不能为空");
|
||
}
|
||
|
||
CrmContract contract = baseMapper.selectById(id);
|
||
if (contract == null) {
|
||
throw new ServiceException("合同不存在");
|
||
}
|
||
if ("finished".equals(contract.getStatus())) {
|
||
throw new ServiceException("已完成的合同不能作废");
|
||
}
|
||
|
||
CrmContract update = new CrmContract();
|
||
update.setId(id);
|
||
update.setStatus("invalid");
|
||
update.setInvalidReason(invalidReason);
|
||
update.setInvalidTime(LocalDateTime.now());
|
||
update.setRemark(remark);
|
||
return baseMapper.updateById(update) > 0;
|
||
}
|
||
|
||
@Override
|
||
public Boolean uploadFile(Long id, String fileUrl) {
|
||
CrmContract update = new CrmContract();
|
||
update.setId(id);
|
||
update.setContractFile(fileUrl);
|
||
return baseMapper.updateById(update) > 0;
|
||
}
|
||
|
||
@Override
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public Boolean uploadAttachments(Long id, String ossIds) {
|
||
if (StringUtils.isBlank(ossIds)) {
|
||
throw new ServiceException("附件ossId不能为空");
|
||
}
|
||
|
||
CrmContract contract = baseMapper.selectById(id);
|
||
if (contract == null) {
|
||
throw new ServiceException("合同不存在");
|
||
}
|
||
|
||
// 追加附件ossId
|
||
String existingFiles = contract.getAttachmentFiles();
|
||
String newFiles;
|
||
if (StringUtils.isBlank(existingFiles)) {
|
||
newFiles = ossIds;
|
||
} else {
|
||
// 将现有附件和新附件合并(去重)
|
||
String[] existingArr = existingFiles.split(",");
|
||
String[] newArr = ossIds.split(",");
|
||
java.util.Set<String> fileSet = new java.util.HashSet<>(java.util.Arrays.asList(existingArr));
|
||
fileSet.addAll(java.util.Arrays.asList(newArr));
|
||
newFiles = String.join(",", fileSet);
|
||
}
|
||
|
||
CrmContract update = new CrmContract();
|
||
update.setId(id);
|
||
update.setAttachmentFiles(newFiles);
|
||
return baseMapper.updateById(update) > 0;
|
||
}
|
||
|
||
@Override
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public Boolean uploadAttachmentsWithCategory(Long id, java.util.List<java.util.Map<String, Object>> attachments) {
|
||
if (attachments == null || attachments.isEmpty()) {
|
||
throw new ServiceException("附件列表不能为空");
|
||
}
|
||
|
||
CrmContract contract = baseMapper.selectById(id);
|
||
if (contract == null) {
|
||
throw new ServiceException("合同不存在");
|
||
}
|
||
|
||
// 解析现有附件(支持JSON格式和逗号分隔格式)
|
||
JSONArray existingAttachments = new JSONArray();
|
||
String existingFiles = contract.getAttachmentFiles();
|
||
if (StringUtils.isNotBlank(existingFiles)) {
|
||
// 尝试解析为JSON格式
|
||
if (existingFiles.trim().startsWith("[")) {
|
||
existingAttachments = JSONUtil.parseArray(existingFiles);
|
||
} else {
|
||
// 兼容旧格式:逗号分隔的ossId,转换为JSON格式
|
||
String[] ossIds = existingFiles.split(",");
|
||
for (String ossId : ossIds) {
|
||
if (StringUtils.isNotBlank(ossId)) {
|
||
JSONObject item = new JSONObject();
|
||
item.set("ossId", ossId.trim());
|
||
item.set("category", "other"); // 旧数据默认分类为"其他附件"
|
||
existingAttachments.add(item);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 添加新附件(去重:基于ossId)
|
||
java.util.Set<String> existingOssIds = new java.util.HashSet<>();
|
||
for (int i = 0; i < existingAttachments.size(); i++) {
|
||
JSONObject item = existingAttachments.getJSONObject(i);
|
||
String ossId = item.getStr("ossId");
|
||
if (StringUtils.isNotBlank(ossId)) {
|
||
existingOssIds.add(ossId);
|
||
}
|
||
}
|
||
|
||
for (java.util.Map<String, Object> attachment : attachments) {
|
||
String ossId = attachment.get("ossId") != null ? attachment.get("ossId").toString() : null;
|
||
String category = attachment.get("category") != null ? attachment.get("category").toString() : "other";
|
||
|
||
if (StringUtils.isBlank(ossId)) {
|
||
continue;
|
||
}
|
||
|
||
// 如果已存在,更新分类;否则添加新附件
|
||
if (existingOssIds.contains(ossId)) {
|
||
// 更新现有附件的分类
|
||
for (int i = 0; i < existingAttachments.size(); i++) {
|
||
JSONObject item = existingAttachments.getJSONObject(i);
|
||
if (ossId.equals(item.getStr("ossId"))) {
|
||
item.set("category", category);
|
||
break;
|
||
}
|
||
}
|
||
} else {
|
||
// 添加新附件
|
||
JSONObject newItem = new JSONObject();
|
||
newItem.set("ossId", ossId);
|
||
newItem.set("category", category);
|
||
existingAttachments.add(newItem);
|
||
existingOssIds.add(ossId);
|
||
}
|
||
}
|
||
|
||
// 保存为JSON格式
|
||
String newFiles = JSONUtil.toJsonStr(existingAttachments);
|
||
|
||
CrmContract update = new CrmContract();
|
||
update.setId(id);
|
||
update.setAttachmentFiles(newFiles);
|
||
return baseMapper.updateById(update) > 0;
|
||
}
|
||
|
||
@Override
|
||
public Map<String, Object> statisticsOverview(Long ownerUserId, String beginTime, String endTime) {
|
||
Map<String, Object> result = new HashMap<>();
|
||
|
||
// 构建查询条件
|
||
LambdaQueryWrapper<CrmContract> lqw = Wrappers.lambdaQuery();
|
||
|
||
// 负责人筛选
|
||
if (ownerUserId != null) {
|
||
lqw.eq(CrmContract::getOwnerUserId, ownerUserId);
|
||
}
|
||
|
||
// 时间范围筛选
|
||
if (StringUtils.isNotBlank(beginTime) && StringUtils.isNotBlank(endTime)) {
|
||
lqw.between(CrmContract::getCreateTime, beginTime, endTime);
|
||
}
|
||
|
||
// 查询所有合同
|
||
List<CrmContract> contracts = baseMapper.selectList(lqw);
|
||
|
||
// 统计各状态数量
|
||
long totalCount = contracts.size();
|
||
long draftCount = contracts.stream().filter(c -> "draft".equals(c.getStatus())).count();
|
||
long approvingCount = contracts.stream().filter(c -> "approving".equals(c.getStatus())).count();
|
||
long executingCount = contracts.stream().filter(c -> "executing".equals(c.getStatus())).count();
|
||
long finishedCount = contracts.stream().filter(c -> "finished".equals(c.getStatus())).count();
|
||
long invalidCount = contracts.stream().filter(c -> "invalid".equals(c.getStatus())).count();
|
||
|
||
// 统计金额
|
||
BigDecimal totalAmount = contracts.stream()
|
||
.map(CrmContract::getAmount)
|
||
.filter(Objects::nonNull)
|
||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||
|
||
BigDecimal receivedAmount = contracts.stream()
|
||
.map(CrmContract::getReceivedAmount)
|
||
.filter(Objects::nonNull)
|
||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||
|
||
BigDecimal unreceivedAmount = contracts.stream()
|
||
.map(CrmContract::getUnreceivedAmount)
|
||
.filter(Objects::nonNull)
|
||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||
|
||
BigDecimal invoiceAmount = contracts.stream()
|
||
.map(CrmContract::getInvoiceAmount)
|
||
.filter(Objects::nonNull)
|
||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||
|
||
BigDecimal uninvoiceAmount = contracts.stream()
|
||
.map(CrmContract::getUninvoiceAmount)
|
||
.filter(Objects::nonNull)
|
||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||
|
||
// 计算回款率和开票率
|
||
BigDecimal receivedRate = totalAmount.compareTo(BigDecimal.ZERO) > 0
|
||
? receivedAmount.divide(totalAmount, 4, RoundingMode.HALF_UP).multiply(new BigDecimal("100"))
|
||
: BigDecimal.ZERO;
|
||
|
||
BigDecimal invoiceRate = totalAmount.compareTo(BigDecimal.ZERO) > 0
|
||
? invoiceAmount.divide(totalAmount, 4, RoundingMode.HALF_UP).multiply(new BigDecimal("100"))
|
||
: BigDecimal.ZERO;
|
||
|
||
// 组装结果
|
||
result.put("totalCount", totalCount);
|
||
result.put("draftCount", draftCount);
|
||
result.put("approvingCount", approvingCount);
|
||
result.put("executingCount", executingCount);
|
||
result.put("finishedCount", finishedCount);
|
||
result.put("invalidCount", invalidCount);
|
||
result.put("totalAmount", totalAmount);
|
||
result.put("receivedAmount", receivedAmount);
|
||
result.put("unreceivedAmount", unreceivedAmount);
|
||
result.put("receivedRate", receivedRate.setScale(2, RoundingMode.HALF_UP));
|
||
result.put("invoiceAmount", invoiceAmount);
|
||
result.put("uninvoiceAmount", uninvoiceAmount);
|
||
result.put("invoiceRate", invoiceRate.setScale(2, RoundingMode.HALF_UP));
|
||
|
||
return result;
|
||
}
|
||
|
||
@Override
|
||
public List<Map<String, Object>> statisticsByStatus() {
|
||
List<Map<String, Object>> result = new ArrayList<>();
|
||
|
||
// 查询所有合同
|
||
List<CrmContract> contracts = baseMapper.selectList(Wrappers.lambdaQuery());
|
||
|
||
// 计算总数和总金额
|
||
long totalCount = contracts.size();
|
||
BigDecimal totalAmount = contracts.stream()
|
||
.map(CrmContract::getAmount)
|
||
.filter(Objects::nonNull)
|
||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||
|
||
// 定义状态映射
|
||
Map<String, String> statusMap = new LinkedHashMap<>();
|
||
statusMap.put("draft", "草稿");
|
||
statusMap.put("approving", "审批中");
|
||
statusMap.put("approved", "已审批");
|
||
statusMap.put("executing", "执行中");
|
||
statusMap.put("finished", "已完成");
|
||
statusMap.put("invalid", "已作废");
|
||
|
||
// 按状态统计
|
||
for (Map.Entry<String, String> entry : statusMap.entrySet()) {
|
||
String status = entry.getKey();
|
||
String statusName = entry.getValue();
|
||
|
||
List<CrmContract> statusContracts = contracts.stream()
|
||
.filter(c -> status.equals(c.getStatus()))
|
||
.toList();
|
||
|
||
long count = statusContracts.size();
|
||
BigDecimal amount = statusContracts.stream()
|
||
.map(CrmContract::getAmount)
|
||
.filter(Objects::nonNull)
|
||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||
|
||
// 计算百分比
|
||
BigDecimal percent = totalCount > 0
|
||
? new BigDecimal(count).divide(new BigDecimal(totalCount), 4, RoundingMode.HALF_UP)
|
||
.multiply(new BigDecimal("100"))
|
||
: BigDecimal.ZERO;
|
||
|
||
Map<String, Object> item = new HashMap<>();
|
||
item.put("status", status);
|
||
item.put("statusName", statusName);
|
||
item.put("count", count);
|
||
item.put("amount", amount);
|
||
item.put("percent", percent.setScale(2, RoundingMode.HALF_UP));
|
||
|
||
result.add(item);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
@Override
|
||
public List<Map<String, Object>> statisticsByType() {
|
||
List<Map<String, Object>> result = new ArrayList<>();
|
||
|
||
// 查询所有合同
|
||
List<CrmContract> contracts = baseMapper.selectList(Wrappers.lambdaQuery());
|
||
|
||
// 计算总数和总金额
|
||
long totalCount = contracts.size();
|
||
BigDecimal totalAmount = contracts.stream()
|
||
.map(CrmContract::getAmount)
|
||
.filter(Objects::nonNull)
|
||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||
|
||
// 定义类型映射
|
||
Map<String, String> typeMap = new LinkedHashMap<>();
|
||
typeMap.put("sale", "销售合同");
|
||
typeMap.put("service", "服务合同");
|
||
typeMap.put("purchase", "采购合同");
|
||
typeMap.put("framework", "框架合同");
|
||
typeMap.put("supplement", "补充协议");
|
||
typeMap.put("lease", "租赁合同");
|
||
typeMap.put("other", "其他");
|
||
|
||
// 按类型统计
|
||
for (Map.Entry<String, String> entry : typeMap.entrySet()) {
|
||
String type = entry.getKey();
|
||
String typeName = entry.getValue();
|
||
|
||
List<CrmContract> typeContracts = contracts.stream()
|
||
.filter(c -> type.equals(c.getContractType()))
|
||
.toList();
|
||
|
||
long count = typeContracts.size();
|
||
|
||
// 如果该类型没有合同,跳过
|
||
if (count == 0) {
|
||
continue;
|
||
}
|
||
|
||
BigDecimal amount = typeContracts.stream()
|
||
.map(CrmContract::getAmount)
|
||
.filter(Objects::nonNull)
|
||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||
|
||
// 计算百分比
|
||
BigDecimal percent = totalCount > 0
|
||
? new BigDecimal(count).divide(new BigDecimal(totalCount), 4, RoundingMode.HALF_UP)
|
||
.multiply(new BigDecimal("100"))
|
||
: BigDecimal.ZERO;
|
||
|
||
Map<String, Object> item = new HashMap<>();
|
||
item.put("contractType", type);
|
||
item.put("typeName", typeName);
|
||
item.put("count", count);
|
||
item.put("amount", amount);
|
||
item.put("percent", percent.setScale(2, RoundingMode.HALF_UP));
|
||
|
||
result.add(item);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
@Override
|
||
public List<Map<String, Object>> statisticsTrend(String type, String beginTime, String endTime) {
|
||
List<Map<String, Object>> result = new ArrayList<>();
|
||
|
||
// 解析开始和结束时间
|
||
LocalDateTime startDateTime;
|
||
LocalDateTime endDateTime;
|
||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||
|
||
try {
|
||
startDateTime = LocalDateTime.parse(beginTime, formatter);
|
||
endDateTime = LocalDateTime.parse(endTime, formatter);
|
||
} catch (Exception e) {
|
||
log.error("日期格式错误", e);
|
||
return result;
|
||
}
|
||
|
||
// 查询时间范围内的合同
|
||
LambdaQueryWrapper<CrmContract> lqw = Wrappers.lambdaQuery();
|
||
lqw.between(CrmContract::getCreateTime, startDateTime, endDateTime);
|
||
List<CrmContract> contracts = baseMapper.selectList(lqw);
|
||
|
||
// 根据type确定统计粒度
|
||
String period;
|
||
if ("month".equals(type)) {
|
||
period = "month";
|
||
} else if ("quarter".equals(type)) {
|
||
period = "quarter";
|
||
} else if ("year".equals(type)) {
|
||
period = "year";
|
||
} else {
|
||
period = "month"; // 默认按月统计
|
||
}
|
||
|
||
// 按时间分组统计
|
||
Map<String, List<CrmContract>> groupedContracts = new LinkedHashMap<>();
|
||
|
||
for (CrmContract contract : contracts) {
|
||
if (contract.getCreateTime() == null) {
|
||
continue;
|
||
}
|
||
|
||
// 将Date转换为LocalDateTime
|
||
LocalDateTime createTime = DateUtil.toLocalDateTime(contract.getCreateTime());
|
||
|
||
String periodKey;
|
||
if ("month".equals(period)) {
|
||
periodKey = createTime.format(DateTimeFormatter.ofPattern("yyyy-MM"));
|
||
} else if ("quarter".equals(period)) {
|
||
int quarter = (createTime.getMonthValue() - 1) / 3 + 1;
|
||
periodKey = createTime.getYear() + "-Q" + quarter;
|
||
} else { // year
|
||
periodKey = String.valueOf(createTime.getYear());
|
||
}
|
||
|
||
groupedContracts.computeIfAbsent(periodKey, k -> new ArrayList<>()).add(contract);
|
||
}
|
||
|
||
// 生成统计结果
|
||
for (Map.Entry<String, List<CrmContract>> entry : groupedContracts.entrySet()) {
|
||
String periodKey = entry.getKey();
|
||
List<CrmContract> periodContracts = entry.getValue();
|
||
|
||
long totalCount = periodContracts.size();
|
||
BigDecimal totalAmount = periodContracts.stream()
|
||
.map(CrmContract::getAmount)
|
||
.filter(Objects::nonNull)
|
||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||
|
||
BigDecimal receivedAmount = periodContracts.stream()
|
||
.map(CrmContract::getReceivedAmount)
|
||
.filter(Objects::nonNull)
|
||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||
|
||
// 计算回款率
|
||
BigDecimal receivedRate = totalAmount.compareTo(BigDecimal.ZERO) > 0
|
||
? receivedAmount.divide(totalAmount, 4, RoundingMode.HALF_UP).multiply(new BigDecimal("100"))
|
||
: BigDecimal.ZERO;
|
||
|
||
Map<String, Object> item = new HashMap<>();
|
||
item.put("period", periodKey);
|
||
item.put("totalCount", totalCount);
|
||
item.put("totalAmount", totalAmount);
|
||
item.put("receivedAmount", receivedAmount);
|
||
item.put("receivedRate", receivedRate.setScale(2, RoundingMode.HALF_UP));
|
||
|
||
result.add(item);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
@Override
|
||
public List<CrmContractVo> queryExpiring(Integer days) {
|
||
if (days == null || days <= 0) {
|
||
days = 30;
|
||
}
|
||
LocalDate endDate = LocalDate.now().plusDays(days);
|
||
|
||
LambdaQueryWrapper<CrmContract> lqw = Wrappers.lambdaQuery();
|
||
lqw.eq(CrmContract::getStatus, "executing");
|
||
lqw.le(CrmContract::getEndDate, endDate);
|
||
lqw.ge(CrmContract::getEndDate, LocalDate.now());
|
||
lqw.orderByAsc(CrmContract::getEndDate);
|
||
|
||
return baseMapper.selectVoList(lqw);
|
||
}
|
||
|
||
/**
|
||
* 生成合同名称
|
||
* <p>销售合同、框架合同:供方简称-客户简称(合同编号);采购合同:客户简称-供方简称(合同编号);其余类型未填名称时为「合同(编号)」。</p>
|
||
*
|
||
* @param contractNo 合同编号
|
||
* @param contractType 合同类型
|
||
* @param supplierShortName 供方简称
|
||
* @param customerShortName 客户简称
|
||
* @param userContractName 用户填写的合同名称(如果为空则自动生成)
|
||
* @return 合同名称
|
||
*/
|
||
private String generateContractName(String contractNo, String contractType,
|
||
String supplierShortName, String customerShortName,
|
||
String userContractName) {
|
||
if (StringUtils.isBlank(userContractName)) {
|
||
// 用户未填写合同名称,根据类型自动生成
|
||
if ("sale".equals(contractType) || "framework".equals(contractType)) {
|
||
// 销售合同、框架合同:供方简称-客户简称(合同编号)
|
||
return supplierShortName + "-" + customerShortName + "(" + contractNo + ")";
|
||
} else if ("purchase".equals(contractType)) {
|
||
// 采购合同:客户简称-供方简称(合同编号)
|
||
return customerShortName + "-" + supplierShortName + "(" + contractNo + ")";
|
||
} else {
|
||
// 其他类型:使用合同编号作为名称
|
||
return "合同(" + contractNo + ")";
|
||
}
|
||
} else {
|
||
// 用户手工填写了合同名称,自动补充合同编号
|
||
// 先移除旧的合同编号(如果存在)
|
||
String cleanedName = userContractName.replaceAll("([^)]+)$", "");
|
||
// 检查是否已经包含新的合同编号,避免重复添加
|
||
if (!cleanedName.contains("(" + contractNo + ")")) {
|
||
return cleanedName + "(" + contractNo + ")";
|
||
}
|
||
return cleanedName;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成合同编号
|
||
* 格式:{供方编码}/X-JT + yyyyMMdd + 3位流水号
|
||
* 示例:SX/X-JT20250926002
|
||
*
|
||
* @param supplierId 供方主体ID,如果为null则使用默认编码"SX"
|
||
* @param signDate 签订日期,如果为null则使用当前日期
|
||
*/
|
||
@Override
|
||
public String generateContractNo(Long supplierId, LocalDate signDate) {
|
||
// 获取供方编码,如果没有则使用默认值"SX"
|
||
String supplierCode = "SX";
|
||
if (supplierId != null) {
|
||
try {
|
||
CrmSupplierInfoVo supplierInfo = supplierInfoService.queryById(supplierId);
|
||
if (supplierInfo != null && StringUtils.isNotBlank(supplierInfo.getSupplierCode())) {
|
||
supplierCode = supplierInfo.getSupplierCode();
|
||
}
|
||
} catch (Exception e) {
|
||
log.warn("查询供方信息失败,使用默认编码,供方ID: {}", supplierId, e);
|
||
}
|
||
}
|
||
|
||
String prefix = supplierCode + "/X-JT";
|
||
// 使用签订日期,如果为null则使用当前日期
|
||
LocalDate dateToUse = signDate != null ? signDate : LocalDate.now();
|
||
String date = dateToUse.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||
|
||
// 查询该签订日期最大流水号(使用动态前缀)
|
||
LambdaQueryWrapper<CrmContract> wrapper = Wrappers.lambdaQuery();
|
||
wrapper.likeRight(CrmContract::getContractNo, prefix + date);
|
||
wrapper.orderByDesc(CrmContract::getContractNo);
|
||
wrapper.last("LIMIT 1");
|
||
CrmContract lastContract = baseMapper.selectOne(wrapper);
|
||
|
||
int serialNum = 1;
|
||
if (lastContract != null && lastContract.getContractNo() != null) {
|
||
String lastNo = lastContract.getContractNo();
|
||
// 完整编号格式:{供方编码}/X-JT20250926002
|
||
// 前缀长度可能不同,需要动态计算
|
||
int prefixLength = prefix.length();
|
||
int dateLength = 8; // yyyyMMdd
|
||
int totalPrefixLength = prefixLength + dateLength;
|
||
|
||
if (lastNo.length() >= totalPrefixLength + 3) {
|
||
try {
|
||
String serialStr = lastNo.substring(totalPrefixLength);
|
||
serialNum = Integer.parseInt(serialStr) + 1;
|
||
} catch (NumberFormatException e) {
|
||
log.warn("解析合同编号流水号失败,使用默认值1,合同编号: {}", lastNo);
|
||
serialNum = 1;
|
||
}
|
||
}
|
||
}
|
||
return String.format("%s%s%03d", prefix, date, serialNum);
|
||
}
|
||
|
||
@Override
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public Boolean updateReceivedAmount(Long contractId) {
|
||
// 查询该合同的所有回款记录
|
||
LambdaQueryWrapper<CrmPayment> lqw = Wrappers.lambdaQuery();
|
||
lqw.eq(CrmPayment::getContractId, contractId);
|
||
lqw.eq(CrmPayment::getDelFlag, 0);
|
||
List<CrmPayment> payments = paymentMapper.selectList(lqw);
|
||
|
||
// 计算已回款金额
|
||
BigDecimal receivedAmount = payments.stream()
|
||
.map(CrmPayment::getAmount)
|
||
.filter(Objects::nonNull)
|
||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||
|
||
// 查询合同
|
||
CrmContract contract = baseMapper.selectById(contractId);
|
||
if (contract == null) {
|
||
log.error("合同不存在,无法更新回款金额,合同ID: {}", contractId);
|
||
return false;
|
||
}
|
||
|
||
// 更新合同回款金额
|
||
CrmContract update = new CrmContract();
|
||
update.setId(contractId);
|
||
update.setReceivedAmount(receivedAmount);
|
||
update.setUnreceivedAmount(contract.getAmount().subtract(receivedAmount));
|
||
|
||
boolean flag = baseMapper.updateById(update) > 0;
|
||
if (flag) {
|
||
log.info("更新合同回款金额成功,合同ID: {}, 已回款: {}", contractId, receivedAmount);
|
||
}
|
||
|
||
return flag;
|
||
}
|
||
|
||
@Override
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public Long renewContract(Long parentContractId, CrmContractBo bo) {
|
||
// 1. 查询原合同
|
||
CrmContract parentContract = baseMapper.selectById(parentContractId);
|
||
if (parentContract == null) {
|
||
throw new ServiceException("原合同不存在");
|
||
}
|
||
|
||
// 2. 创建续签合同
|
||
CrmContract newContract = new CrmContract();
|
||
|
||
// 从原合同复制基础信息
|
||
newContract.setCustomerId(parentContract.getCustomerId());
|
||
newContract.setCustomerName(parentContract.getCustomerName());
|
||
newContract.setContactId(parentContract.getContactId());
|
||
newContract.setContactName(parentContract.getContactName());
|
||
newContract.setContactMobile(parentContract.getContactMobile());
|
||
newContract.setContractType(parentContract.getContractType());
|
||
newContract.setOwnerUserId(parentContract.getOwnerUserId());
|
||
newContract.setOwnerUserName(parentContract.getOwnerUserName());
|
||
// 复制供方主体ID
|
||
newContract.setSupplierId(bo.getSupplierId() != null ? bo.getSupplierId() : parentContract.getSupplierId());
|
||
|
||
// 设置续签相关字段
|
||
newContract.setContractNature("renewal");
|
||
newContract.setParentContractId(parentContractId);
|
||
newContract
|
||
.setRenewalCount((parentContract.getRenewalCount() == null ? 0 : parentContract.getRenewalCount()) + 1);
|
||
|
||
// 使用传入的新信息覆盖
|
||
if (StringUtils.isNotBlank(bo.getContractName())) {
|
||
newContract.setContractName(bo.getContractName());
|
||
} else {
|
||
newContract.setContractName(parentContract.getContractName() + "-续签" + newContract.getRenewalCount());
|
||
}
|
||
|
||
newContract.setAmount(bo.getAmount() != null ? bo.getAmount() : parentContract.getAmount());
|
||
newContract.setCurrency(bo.getCurrency() != null ? bo.getCurrency() : parentContract.getCurrency());
|
||
newContract.setTaxRate(bo.getTaxRate() != null ? bo.getTaxRate() : parentContract.getTaxRate());
|
||
|
||
// 计算含税金额
|
||
if (newContract.getTaxRate() != null && newContract.getAmount() != null) {
|
||
newContract.setAmountWithTax(
|
||
newContract.getAmount().multiply(
|
||
BigDecimal.ONE.add(
|
||
newContract.getTaxRate().divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP))));
|
||
}
|
||
|
||
newContract.setSignDate(bo.getSignDate());
|
||
newContract.setStartDate(bo.getStartDate());
|
||
newContract.setEndDate(bo.getEndDate());
|
||
newContract.setContractPeriod(bo.getContractPeriod());
|
||
|
||
// 复制甲乙方信息
|
||
newContract.setPartyA(parentContract.getPartyA());
|
||
newContract.setPartyAAddress(parentContract.getPartyAAddress());
|
||
newContract.setPartyAContact(parentContract.getPartyAContact());
|
||
newContract.setPartyAMobile(parentContract.getPartyAMobile());
|
||
newContract.setPartyB(parentContract.getPartyB());
|
||
newContract.setPartyBAddress(parentContract.getPartyBAddress());
|
||
newContract.setPartyBContact(parentContract.getPartyBContact());
|
||
newContract.setPartyBMobile(parentContract.getPartyBMobile());
|
||
|
||
// 复制合同条款
|
||
newContract.setPaymentTerms(
|
||
bo.getPaymentTerms() != null ? bo.getPaymentTerms() : parentContract.getPaymentTerms());
|
||
newContract.setDeliveryTerms(
|
||
bo.getDeliveryTerms() != null ? bo.getDeliveryTerms() : parentContract.getDeliveryTerms());
|
||
newContract.setContractTerms(
|
||
bo.getContractTerms() != null ? bo.getContractTerms() : parentContract.getContractTerms());
|
||
|
||
// 生成合同编号(续签合同使用原合同的供方主体)
|
||
newContract.setContractNo(generateContractNo(newContract.getSupplierId(), newContract.getSignDate()));
|
||
|
||
// 设置默认状态
|
||
newContract.setStatus("draft");
|
||
newContract.setApprovalStatus(null); // 草稿状态下,审批状态应该为空
|
||
newContract.setPerformanceStatus("normal");
|
||
newContract.setPerformanceRate(BigDecimal.ZERO);
|
||
newContract.setReceivedAmount(BigDecimal.ZERO);
|
||
newContract.setUnreceivedAmount(newContract.getAmount());
|
||
newContract.setInvoiceAmount(BigDecimal.ZERO);
|
||
newContract.setUninvoiceAmount(newContract.getAmount());
|
||
|
||
// 保存续签合同
|
||
boolean flag = baseMapper.insert(newContract) > 0;
|
||
if (!flag) {
|
||
throw new ServiceException("创建续签合同失败");
|
||
}
|
||
|
||
// 复制产品明细
|
||
if (bo.getProducts() != null && !bo.getProducts().isEmpty()) {
|
||
resolveContractProductIds(bo.getProducts());
|
||
// 使用传入的产品
|
||
for (CrmContractProductBo product : bo.getProducts()) {
|
||
product.setContractId(newContract.getId());
|
||
// 清空ID,让MyBatis-Plus自动生成新ID,避免主键冲突
|
||
product.setId(null);
|
||
contractProductService.insertByBo(product);
|
||
if ("framework".equals(parentContract.getContractType())) {
|
||
savePriceTiersForProduct(product.getId(), newContract.getId(), product.getPriceTiers());
|
||
}
|
||
}
|
||
} else {
|
||
// 复制原合同的产品
|
||
LambdaQueryWrapper<CrmContractProduct> productWrapper = Wrappers.lambdaQuery();
|
||
productWrapper.eq(CrmContractProduct::getContractId, parentContractId);
|
||
List<CrmContractProduct> parentProducts = contractProductMapper.selectList(productWrapper);
|
||
|
||
for (CrmContractProduct parentProduct : parentProducts) {
|
||
CrmContractProduct newProduct = new CrmContractProduct();
|
||
newProduct.setContractId(newContract.getId());
|
||
newProduct.setProductId(parentProduct.getProductId());
|
||
newProduct.setProductName(parentProduct.getProductName());
|
||
newProduct.setProductNo(parentProduct.getProductNo());
|
||
newProduct.setCategoryId(parentProduct.getCategoryId());
|
||
newProduct.setCategoryName(parentProduct.getCategoryName());
|
||
newProduct.setModel(parentProduct.getModel());
|
||
newProduct.setUnit(parentProduct.getUnit());
|
||
newProduct.setQuantity(parentProduct.getQuantity());
|
||
newProduct.setPrice(parentProduct.getPrice());
|
||
newProduct.setDiscount(parentProduct.getDiscount());
|
||
newProduct.setSubtotal(parentProduct.getSubtotal());
|
||
newProduct.setDescription(parentProduct.getDescription());
|
||
newProduct.setSort(parentProduct.getSort());
|
||
contractProductMapper.insert(newProduct);
|
||
if ("framework".equals(parentContract.getContractType())) {
|
||
copyPriceTiersToNewProduct(parentProduct.getId(), newContract.getId(), newProduct.getId());
|
||
}
|
||
}
|
||
}
|
||
|
||
log.info("合同续签成功,原合同ID: {}, 新合同ID: {}, 续签次数: {}", parentContractId, newContract.getId(),
|
||
newContract.getRenewalCount());
|
||
return newContract.getId();
|
||
}
|
||
|
||
@Override
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public Boolean createFromOpportunity(Long opportunityId, CrmContractBo bo) {
|
||
// 1. 查询商机
|
||
CrmOpportunity opportunity = opportunityMapper.selectById(opportunityId);
|
||
if (opportunity == null) {
|
||
throw new ServiceException("商机不存在");
|
||
}
|
||
|
||
// 2. 自动填充信息
|
||
bo.setOpportunityId(opportunityId);
|
||
bo.setCustomerId(opportunity.getCustomerId());
|
||
if (bo.getAmount() == null) {
|
||
bo.setAmount(opportunity.getAmount());
|
||
}
|
||
if (bo.getOwnerUserId() == null) {
|
||
bo.setOwnerUserId(opportunity.getOwnerUserId());
|
||
}
|
||
|
||
// 3. 创建合同
|
||
boolean flag = insertByBo(bo);
|
||
|
||
// 4. 更新商机状态为"已转合同"
|
||
if (flag) {
|
||
CrmOpportunity updateOpp = new CrmOpportunity();
|
||
updateOpp.setId(opportunityId);
|
||
updateOpp.setStage("won"); // 赢单
|
||
opportunityMapper.updateById(updateOpp);
|
||
|
||
log.info("从商机创建合同成功,商机ID: {}, 合同ID: {}", opportunityId, bo.getId());
|
||
}
|
||
|
||
return flag;
|
||
}
|
||
|
||
/**
|
||
* 保存合同前解析产品ID(按名称/规格/分类/单位/税率匹配,未命中自动新增)
|
||
*/
|
||
private void resolveContractProductIds(List<CrmContractProductBo> products) {
|
||
if (products == null || products.isEmpty()) {
|
||
return;
|
||
}
|
||
for (CrmContractProductBo product : products) {
|
||
product.setProductId(resolveProductIdForContractItem(product));
|
||
}
|
||
}
|
||
|
||
private Long resolveProductIdForContractItem(CrmContractProductBo contractProductBo) {
|
||
if (contractProductBo == null) {
|
||
throw new ServiceException("产品明细不能为空");
|
||
}
|
||
String productName = normalizeText(contractProductBo.getProductName());
|
||
if (productName == null) {
|
||
throw new ServiceException("产品名称不能为空");
|
||
}
|
||
String model = normalizeText(contractProductBo.getModel());
|
||
String unit = normalizeText(contractProductBo.getUnit());
|
||
BigDecimal taxRateForCompare = normalizeTaxRateForCompare(contractProductBo.getTaxRate());
|
||
BigDecimal taxRateForSave = normalizeTaxRateForSave(contractProductBo.getTaxRate());
|
||
|
||
LambdaQueryWrapper<CrmProduct> lqw = Wrappers.lambdaQuery();
|
||
// 名称/规格/单位:trim + 不区分大小写 + null与空字符串视为相同
|
||
lqw.apply("LOWER(TRIM(COALESCE(product_name, ''))) = {0}", toLower(productName));
|
||
lqw.apply("LOWER(TRIM(COALESCE(model, ''))) = {0}", toLower(model));
|
||
lqw.apply("LOWER(TRIM(COALESCE(unit, ''))) = {0}", toLower(unit));
|
||
// 税率:统一保留2位后比较
|
||
lqw.apply("ROUND(COALESCE(tax_rate, 0), 2) = {0}", taxRateForCompare);
|
||
lqw.orderByDesc(CrmProduct::getId);
|
||
List<CrmProduct> candidates = productMapper.selectList(lqw);
|
||
|
||
if (!candidates.isEmpty()) {
|
||
Long categoryId = contractProductBo.getCategoryId();
|
||
// 分类为空允许匹配;若有分类,优先取分类一致
|
||
if (categoryId != null) {
|
||
for (CrmProduct candidate : candidates) {
|
||
if (categoryId.equals(candidate.getCategoryId())) {
|
||
return candidate.getId();
|
||
}
|
||
}
|
||
}
|
||
// 命中多条取最新一条(id最大)
|
||
return candidates.get(0).getId();
|
||
}
|
||
|
||
// 未命中则自动新增产品
|
||
CrmProductBo newBo = new CrmProductBo();
|
||
newBo.setProductNo(productService.generateAutoProductNo());
|
||
newBo.setProductName(productName);
|
||
newBo.setCategoryId(contractProductBo.getCategoryId());
|
||
newBo.setCategoryName(normalizeText(contractProductBo.getCategoryName()));
|
||
newBo.setModel(model);
|
||
newBo.setUnit(unit);
|
||
newBo.setTaxRate(taxRateForSave);
|
||
// 合同保存自动新增产品时,标准价格默认0
|
||
newBo.setPrice(BigDecimal.ZERO);
|
||
newBo.setStatus("on");
|
||
productService.insertByBo(newBo);
|
||
return newBo.getId();
|
||
}
|
||
|
||
private String normalizeText(String value) {
|
||
if (value == null) {
|
||
return null;
|
||
}
|
||
String trimmed = value.trim();
|
||
return trimmed.isEmpty() ? null : trimmed;
|
||
}
|
||
|
||
private String toLower(String value) {
|
||
return value == null ? "" : value.toLowerCase();
|
||
}
|
||
|
||
private BigDecimal normalizeTaxRateForCompare(BigDecimal value) {
|
||
BigDecimal taxRate = value == null ? BigDecimal.ZERO : value;
|
||
return taxRate.setScale(2, RoundingMode.HALF_UP);
|
||
}
|
||
|
||
/**
|
||
* 保存税率时尽量保留前端传入形态:
|
||
* - 传整数则保存整数
|
||
* - 传小数则保存小数
|
||
*/
|
||
private BigDecimal normalizeTaxRateForSave(BigDecimal value) {
|
||
if (value == null) {
|
||
return BigDecimal.ZERO;
|
||
}
|
||
BigDecimal normalized = value.stripTrailingZeros();
|
||
if (normalized.scale() <= 0) {
|
||
return normalized.setScale(0, RoundingMode.UNNECESSARY);
|
||
}
|
||
return normalized;
|
||
}
|
||
|
||
private void validateContractTypeAndProductsPresence(CrmContractBo bo) {
|
||
if (bo == null || StringUtils.isBlank(bo.getContractType())) {
|
||
return;
|
||
}
|
||
String contractType = bo.getContractType();
|
||
if ("sale".equals(contractType) || "purchase".equals(contractType) || "framework".equals(contractType)) {
|
||
if (bo.getProducts() == null || bo.getProducts().isEmpty()) {
|
||
throw new ServiceException("销售合同、采购合同或框架合同必须至少添加一个产品");
|
||
}
|
||
}
|
||
}
|
||
|
||
private void normalizeFrameworkContractData(CrmContractBo bo) {
|
||
if (bo == null || !"framework".equals(bo.getContractType())) {
|
||
return;
|
||
}
|
||
bo.setAmount(BigDecimal.ZERO);
|
||
bo.setAmountWithTax(BigDecimal.ZERO);
|
||
if (bo.getProducts() == null) {
|
||
return;
|
||
}
|
||
for (CrmContractProductBo p : bo.getProducts()) {
|
||
p.setPrice(BigDecimal.ZERO);
|
||
p.setSubtotal(BigDecimal.ZERO);
|
||
p.setTaxAmount(BigDecimal.ZERO);
|
||
}
|
||
}
|
||
|
||
private void validateProductPricesByContractType(CrmContractBo bo) {
|
||
if (bo == null || StringUtils.isBlank(bo.getContractType())) {
|
||
return;
|
||
}
|
||
String contractType = bo.getContractType();
|
||
if (!"sale".equals(contractType) && !"purchase".equals(contractType)) {
|
||
return;
|
||
}
|
||
List<CrmContractProductBo> products = bo.getProducts();
|
||
if (products == null) {
|
||
return;
|
||
}
|
||
for (CrmContractProductBo p : products) {
|
||
if (p.getPrice() == null || p.getPrice().compareTo(new BigDecimal("0.01")) < 0) {
|
||
throw new ServiceException("销售/采购合同产品的含税单价必须大于0");
|
||
}
|
||
if (p.getSubtotal() == null || p.getSubtotal().compareTo(new BigDecimal("0.01")) < 0) {
|
||
throw new ServiceException("销售/采购合同产品的含税总价必须大于0");
|
||
}
|
||
}
|
||
}
|
||
|
||
private void validateSalePurchaseContractAmount(CrmContractBo bo) {
|
||
if (bo == null || StringUtils.isBlank(bo.getContractType())) {
|
||
return;
|
||
}
|
||
String contractType = bo.getContractType();
|
||
if (!"sale".equals(contractType) && !"purchase".equals(contractType)) {
|
||
return;
|
||
}
|
||
if (bo.getAmount() == null || bo.getAmount().compareTo(new BigDecimal("0.01")) < 0) {
|
||
throw new ServiceException("销售合同或采购合同的合同金额必须大于0");
|
||
}
|
||
}
|
||
|
||
private List<String> parseLetterOssIdsJson(String json) {
|
||
if (StringUtils.isBlank(json)) {
|
||
return Collections.emptyList();
|
||
}
|
||
try {
|
||
List<String> list = JSONUtil.toList(json, String.class);
|
||
return list.stream().filter(StringUtils::isNotBlank).map(String::trim).toList();
|
||
} catch (Exception e) {
|
||
log.warn("解析 letter_oss_ids JSON 失败: {}", json);
|
||
return Collections.emptyList();
|
||
}
|
||
}
|
||
|
||
private List<String> parseLetterOssIdsFromEntity(CrmContractProductPriceTier en) {
|
||
if (StringUtils.isNotBlank(en.getLetterOssIds())) {
|
||
List<String> parsed = parseLetterOssIdsJson(en.getLetterOssIds());
|
||
if (!parsed.isEmpty()) {
|
||
return new ArrayList<>(parsed);
|
||
}
|
||
}
|
||
if (en.getLetterOssId() != null) {
|
||
return new ArrayList<>(List.of(en.getLetterOssId().toString()));
|
||
}
|
||
return new ArrayList<>();
|
||
}
|
||
|
||
private CrmContractProductPriceTierVo convertTierEntityToVo(CrmContractProductPriceTier en) {
|
||
CrmContractProductPriceTierVo vo = new CrmContractProductPriceTierVo();
|
||
vo.setId(en.getId());
|
||
vo.setContractId(en.getContractId());
|
||
vo.setContractProductId(en.getContractProductId());
|
||
vo.setStartDate(en.getStartDate());
|
||
vo.setEndDate(en.getEndDate());
|
||
vo.setPrice(en.getPrice());
|
||
vo.setSort(en.getSort());
|
||
vo.setLetterOssIds(parseLetterOssIdsFromEntity(en));
|
||
return vo;
|
||
}
|
||
|
||
private void savePriceTiersForProduct(Long contractProductId, Long contractId, List<CrmContractProductPriceTierBo> tiers) {
|
||
if (tiers == null || tiers.isEmpty()) {
|
||
return;
|
||
}
|
||
int sort = 1;
|
||
for (CrmContractProductPriceTierBo t : tiers) {
|
||
if (t.getStartDate() == null || t.getEndDate() == null || t.getPrice() == null) {
|
||
continue;
|
||
}
|
||
List<String> ossIds = t.getLetterOssIds();
|
||
if (ossIds == null || ossIds.isEmpty()) {
|
||
continue;
|
||
}
|
||
List<String> normalized = ossIds.stream()
|
||
.filter(StringUtils::isNotBlank)
|
||
.map(String::trim)
|
||
.toList();
|
||
if (normalized.isEmpty()) {
|
||
continue;
|
||
}
|
||
CrmContractProductPriceTier e = new CrmContractProductPriceTier();
|
||
e.setContractId(contractId);
|
||
e.setContractProductId(contractProductId);
|
||
e.setStartDate(t.getStartDate());
|
||
e.setEndDate(t.getEndDate());
|
||
e.setPrice(t.getPrice());
|
||
e.setLetterOssIds(JSONUtil.toJsonStr(normalized));
|
||
e.setLetterOssId(null);
|
||
if (t.getSort() == null) {
|
||
e.setSort(sort);
|
||
} else {
|
||
e.setSort(t.getSort());
|
||
}
|
||
sort++;
|
||
contractProductPriceTierMapper.insert(e);
|
||
}
|
||
}
|
||
|
||
private void copyPriceTiersToNewProduct(Long oldContractProductId, Long newContractId, Long newContractProductId) {
|
||
List<CrmContractProductPriceTier> rows = contractProductPriceTierMapper.selectList(
|
||
Wrappers.lambdaQuery(CrmContractProductPriceTier.class)
|
||
.eq(CrmContractProductPriceTier::getContractProductId, oldContractProductId));
|
||
if (rows == null || rows.isEmpty()) {
|
||
return;
|
||
}
|
||
int s = 1;
|
||
for (CrmContractProductPriceTier row : rows) {
|
||
CrmContractProductPriceTier n = new CrmContractProductPriceTier();
|
||
n.setContractId(newContractId);
|
||
n.setContractProductId(newContractProductId);
|
||
n.setStartDate(row.getStartDate());
|
||
n.setEndDate(row.getEndDate());
|
||
n.setPrice(row.getPrice());
|
||
n.setLetterOssIds(row.getLetterOssIds());
|
||
n.setLetterOssId(row.getLetterOssId());
|
||
n.setSort(row.getSort() != null ? row.getSort() : s);
|
||
contractProductPriceTierMapper.insert(n);
|
||
s++;
|
||
}
|
||
}
|
||
|
||
private void validateFrameworkContractForSubmitApproval(Long contractId, CrmContract contract) {
|
||
if (!"framework".equals(contract.getContractType())) {
|
||
return;
|
||
}
|
||
List<CrmContractProductVo> products = contractProductService.queryListByContractId(contractId);
|
||
if (products == null || products.isEmpty()) {
|
||
throw new ServiceException("框架合同必须维护产品明细");
|
||
}
|
||
List<CrmContractProductPriceTier> tiers = contractProductPriceTierMapper.selectList(
|
||
Wrappers.lambdaQuery(CrmContractProductPriceTier.class)
|
||
.eq(CrmContractProductPriceTier::getContractId, contractId));
|
||
Map<Long, List<CrmContractProductPriceTier>> byProduct = tiers.stream()
|
||
.collect(Collectors.groupingBy(CrmContractProductPriceTier::getContractProductId));
|
||
for (CrmContractProductVo prod : products) {
|
||
List<CrmContractProductPriceTier> list = byProduct.get(prod.getId());
|
||
if (list == null || list.isEmpty()) {
|
||
throw new ServiceException("框架合同产品「" + prod.getProductName() + "」至少维护一条调价区间(含调价函)");
|
||
}
|
||
for (CrmContractProductPriceTier t : list) {
|
||
if (t.getStartDate() == null || t.getEndDate() == null) {
|
||
throw new ServiceException("框架合同产品「" + prod.getProductName() + "」调价区间日期不能为空");
|
||
}
|
||
if (t.getStartDate().isAfter(t.getEndDate())) {
|
||
throw new ServiceException("框架合同产品「" + prod.getProductName() + "」调价开始日期不能晚于结束日期");
|
||
}
|
||
if (t.getPrice() == null || t.getPrice().compareTo(BigDecimal.ZERO) <= 0) {
|
||
throw new ServiceException("框架合同产品「" + prod.getProductName() + "」调价含税单价必须大于0");
|
||
}
|
||
List<String> letterIds = parseLetterOssIdsFromEntity(t);
|
||
if (letterIds.isEmpty()) {
|
||
throw new ServiceException("框架合同产品「" + prod.getProductName() + "」每条调价区间须上传调价函附件");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@Override
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public Boolean createFromQuote(Long quoteId, CrmContractBo bo) {
|
||
// 1. 查询报价单
|
||
CrmQuote quote = quoteMapper.selectById(quoteId);
|
||
if (quote == null) {
|
||
throw new ServiceException("报价单不存在");
|
||
}
|
||
|
||
// 2. 自动填充信息
|
||
bo.setQuoteId(quoteId);
|
||
bo.setCustomerId(quote.getCustomerId());
|
||
bo.setOpportunityId(quote.getOpportunityId());
|
||
|
||
if (bo.getAmount() == null) {
|
||
bo.setAmount(quote.getTotalAmount());
|
||
}
|
||
if (bo.getOwnerUserId() == null) {
|
||
bo.setOwnerUserId(quote.getOwnerUserId());
|
||
}
|
||
|
||
// 从报价单复制产品明细(如果没有传入)
|
||
if ((bo.getProducts() == null || bo.getProducts().isEmpty()) && quote.getId() != null) {
|
||
// TODO: 从报价单产品表复制产品明细
|
||
log.info("从报价单复制产品明细,报价单ID: {}", quoteId);
|
||
}
|
||
|
||
// 3. 创建合同
|
||
boolean flag = insertByBo(bo);
|
||
|
||
// 4. 更新报价单状态为"已转合同"
|
||
if (flag) {
|
||
CrmQuote updateQuote = new CrmQuote();
|
||
updateQuote.setId(quoteId);
|
||
updateQuote.setStatus("converted"); // 已转化
|
||
quoteMapper.updateById(updateQuote);
|
||
|
||
log.info("从报价单创建合同成功,报价单ID: {}, 合同ID: {}", quoteId, bo.getId());
|
||
}
|
||
|
||
return flag;
|
||
}
|
||
|
||
@Override
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public Boolean createFromTemplate(Long templateId, CrmContractBo bo) {
|
||
// 1. 查询模板
|
||
CrmContractTemplate template = contractTemplateMapper.selectById(templateId);
|
||
if (template == null) {
|
||
throw new ServiceException("合同模板不存在");
|
||
}
|
||
|
||
if (template.getIsActive() == null || template.getIsActive() != 1) {
|
||
throw new ServiceException("该模板已停用,无法使用");
|
||
}
|
||
|
||
// 2. 从模板填充信息
|
||
if (bo.getContractType() == null) {
|
||
bo.setContractType(template.getContractType());
|
||
}
|
||
if (bo.getCurrency() == null) {
|
||
bo.setCurrency(template.getDefaultCurrency());
|
||
}
|
||
if (bo.getTaxRate() == null) {
|
||
bo.setTaxRate(template.getDefaultTaxRate());
|
||
}
|
||
if (bo.getContractPeriod() == null) {
|
||
bo.setContractPeriod(template.getDefaultPeriod());
|
||
}
|
||
|
||
// 填充合同条款
|
||
if (StringUtils.isBlank(bo.getPaymentTerms())) {
|
||
bo.setPaymentTerms(template.getPaymentTerms());
|
||
}
|
||
if (StringUtils.isBlank(bo.getDeliveryTerms())) {
|
||
bo.setDeliveryTerms(template.getDeliveryTerms());
|
||
}
|
||
if (StringUtils.isBlank(bo.getContractTerms())) {
|
||
bo.setContractTerms(template.getContractTerms());
|
||
}
|
||
|
||
// 填充乙方信息
|
||
if (StringUtils.isBlank(bo.getPartyB())) {
|
||
bo.setPartyB(template.getPartyB());
|
||
}
|
||
if (StringUtils.isBlank(bo.getPartyBAddress())) {
|
||
bo.setPartyBAddress(template.getPartyBAddress());
|
||
}
|
||
if (StringUtils.isBlank(bo.getPartyBContact())) {
|
||
bo.setPartyBContact(template.getPartyBContact());
|
||
}
|
||
if (StringUtils.isBlank(bo.getPartyBMobile())) {
|
||
bo.setPartyBMobile(template.getPartyBMobile());
|
||
}
|
||
|
||
// 3. 创建合同
|
||
boolean flag = insertByBo(bo);
|
||
|
||
// 4. 更新模板使用次数
|
||
if (flag) {
|
||
CrmContractTemplate updateTemplate = new CrmContractTemplate();
|
||
updateTemplate.setId(templateId);
|
||
updateTemplate.setUsageCount((template.getUsageCount() == null ? 0 : template.getUsageCount()) + 1);
|
||
contractTemplateMapper.updateById(updateTemplate);
|
||
|
||
log.info("从模板创建合同成功,模板ID: {}, 合同ID: {}", templateId, bo.getId());
|
||
}
|
||
|
||
return flag;
|
||
}
|
||
|
||
@Override
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public Boolean signContract(Long id, LocalDate actualSignDate, String contractFile, String remark) {
|
||
// 1. 查询合同
|
||
CrmContract contract = baseMapper.selectById(id);
|
||
if (contract == null) {
|
||
throw new ServiceException("合同不存在");
|
||
}
|
||
|
||
// 2. 校验合同状态
|
||
if (!"approved".equals(contract.getStatus())) {
|
||
throw new ServiceException("只有审批通过的合同才能签订");
|
||
}
|
||
|
||
// 3. 更新合同信息
|
||
CrmContract update = new CrmContract();
|
||
update.setId(id);
|
||
update.setActualSignDate(actualSignDate);
|
||
update.setContractFile(contractFile);
|
||
update.setStatus("executing"); // 签订后进入执行中状态
|
||
update.setPerformanceStatus("normal");
|
||
update.setPerformanceRate(BigDecimal.ZERO);
|
||
if (StringUtils.isNotBlank(remark)) {
|
||
update.setRemark(remark);
|
||
}
|
||
|
||
boolean flag = baseMapper.updateById(update) > 0;
|
||
|
||
if (flag) {
|
||
log.info("合同签订成功,合同ID: {}, 签订日期: {}", id, actualSignDate);
|
||
|
||
// TODO: 发送通知给负责人和财务
|
||
// TODO: 激活回款计划
|
||
}
|
||
|
||
return flag;
|
||
}
|
||
|
||
@Override
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public Boolean batchAssignOwner(Collection<Long> ids, Long ownerUserId, String remark) {
|
||
if (ids == null || ids.isEmpty()) {
|
||
throw new ServiceException("合同ID列表不能为空");
|
||
}
|
||
if (ownerUserId == null) {
|
||
throw new ServiceException("负责人ID不能为空");
|
||
}
|
||
|
||
// 批量更新
|
||
int successCount = 0;
|
||
for (Long id : ids) {
|
||
CrmContract contract = baseMapper.selectById(id);
|
||
if (contract == null) {
|
||
log.warn("合同不存在,跳过,ID: {}", id);
|
||
continue;
|
||
}
|
||
|
||
CrmContract update = new CrmContract();
|
||
update.setId(id);
|
||
update.setOwnerUserId(ownerUserId);
|
||
|
||
// 设置负责人姓名
|
||
try {
|
||
String ownerUserName = userService.selectNicknameById(ownerUserId);
|
||
update.setOwnerUserName(ownerUserName);
|
||
} catch (Exception e) {
|
||
log.warn("获取负责人姓名失败,用户ID: {}", ownerUserId, e);
|
||
update.setOwnerUserName(null);
|
||
}
|
||
|
||
if (StringUtils.isNotBlank(remark)) {
|
||
update.setRemark(remark);
|
||
}
|
||
|
||
if (baseMapper.updateById(update) > 0) {
|
||
successCount++;
|
||
log.info("合同分配成功,合同ID: {}, 新负责人ID: {}", id, ownerUserId);
|
||
}
|
||
}
|
||
|
||
log.info("批量分配合同负责人完成,成功: {}, 总数: {}", successCount, ids.size());
|
||
return successCount > 0;
|
||
}
|
||
|
||
@Override
|
||
public String downloadContractPdf(Long id) {
|
||
// 查询合同
|
||
CrmContractVo contract = queryById(id);
|
||
if (contract == null) {
|
||
throw new ServiceException("合同不存在");
|
||
}
|
||
|
||
// 返回合同文件路径(如果存在)
|
||
if (StringUtils.isNotBlank(contract.getContractFile())) {
|
||
return contract.getContractFile();
|
||
}
|
||
|
||
throw new ServiceException("合同PDF文件不存在");
|
||
}
|
||
|
||
@Override
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public Boolean updateInvoiceAmount(Long contractId) {
|
||
// 查询该合同的所有发票记录
|
||
LambdaQueryWrapper<CrmInvoice> lqw = Wrappers.lambdaQuery();
|
||
lqw.eq(CrmInvoice::getContractId, contractId);
|
||
lqw.eq(CrmInvoice::getDelFlag, 0);
|
||
List<CrmInvoice> invoices = invoiceMapper.selectList(lqw);
|
||
|
||
// 计算已开票金额
|
||
BigDecimal invoiceAmount = invoices.stream()
|
||
.map(CrmInvoice::getAmount)
|
||
.filter(Objects::nonNull)
|
||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||
|
||
// 查询合同
|
||
CrmContract contract = baseMapper.selectById(contractId);
|
||
if (contract == null) {
|
||
log.error("合同不存在,无法更新开票金额,合同ID: {}", contractId);
|
||
return false;
|
||
}
|
||
|
||
// 更新合同开票金额
|
||
CrmContract update = new CrmContract();
|
||
update.setId(contractId);
|
||
update.setInvoiceAmount(invoiceAmount);
|
||
update.setUninvoiceAmount(contract.getAmount().subtract(invoiceAmount));
|
||
|
||
boolean flag = baseMapper.updateById(update) > 0;
|
||
if (flag) {
|
||
log.info("更新合同开票金额成功,合同ID: {}, 已开票: {}", contractId, invoiceAmount);
|
||
}
|
||
|
||
return flag;
|
||
}
|
||
}
|