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

2015 lines
86 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package org.dromara.crm.service.impl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.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;
}
}