package cn.iocoder.yudao.module.crm.service.bidbond;
|
|
import cn.hutool.core.collection.CollUtil;
|
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
import cn.iocoder.yudao.module.crm.controller.admin.bidbond.vo.CrmBidBondPageReqVO;
|
import cn.iocoder.yudao.module.crm.controller.admin.bidbond.vo.CrmBidBondRespVO;
|
import cn.iocoder.yudao.module.crm.controller.admin.bidbond.vo.CrmBidBondSaleOrderRespVO;
|
import cn.iocoder.yudao.module.crm.controller.admin.bidbond.vo.CrmBidBondSaveReqVO;
|
import cn.iocoder.yudao.module.crm.dal.dataobject.bidbond.CrmBidBondDO;
|
import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
|
import cn.iocoder.yudao.module.crm.dal.mysql.bidbond.CrmBidBondMapper;
|
import cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants;
|
import cn.iocoder.yudao.module.crm.enums.bidbond.CrmBidBondRefundStatusEnum;
|
import cn.iocoder.yudao.module.crm.enums.bidbond.CrmBidResultEnum;
|
import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
|
import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi;
|
import cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderRespDTO;
|
import jakarta.annotation.Resource;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.validation.annotation.Validated;
|
|
import java.math.BigDecimal;
|
import java.time.LocalDate;
|
import java.time.format.DateTimeFormatter;
|
import java.util.*;
|
import java.util.stream.Collectors;
|
|
/**
|
* CRM 投标保证金 Service 实现
|
*/
|
@Service
|
@Validated
|
@Slf4j
|
public class CrmBidBondServiceImpl implements CrmBidBondService {
|
|
/**
|
* ERP 销售订单已审核状态
|
*/
|
private static final int ERP_SALE_ORDER_STATUS_APPROVE = 20;
|
|
private static final DateTimeFormatter NO_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMM");
|
|
@Resource
|
private CrmBidBondMapper bidBondMapper;
|
@Resource
|
private CrmCustomerService customerService;
|
@Resource
|
private ErpSaleOrderApi erpSaleOrderApi;
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public Long createBidBond(CrmBidBondSaveReqVO createReqVO) {
|
CrmBidBondDO bond = validateAndBuild(createReqVO);
|
bond.setId(null);
|
bond.setNo(generateBondNo());
|
bidBondMapper.insert(bond);
|
return bond.getId();
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void updateBidBond(CrmBidBondSaveReqVO updateReqVO) {
|
validateBidBondExists(updateReqVO.getId());
|
CrmBidBondDO updateObj = validateAndBuild(updateReqVO);
|
updateObj.setId(updateReqVO.getId());
|
// 单号不允许修改,保持原值
|
updateObj.setNo(null);
|
bidBondMapper.updateByIdIncludingNulls(updateObj);
|
}
|
|
@Override
|
public void deleteBidBond(Long id) {
|
validateBidBondExists(id);
|
bidBondMapper.deleteById(id);
|
}
|
|
@Override
|
public CrmBidBondDO getBidBond(Long id) {
|
return bidBondMapper.selectById(id);
|
}
|
|
@Override
|
public CrmBidBondRespVO getBidBondDetail(Long id) {
|
CrmBidBondDO bond = validateBidBondExists(id);
|
return buildRespList(List.of(bond)).get(0);
|
}
|
|
@Override
|
public PageResult<CrmBidBondRespVO> getBidBondPage(CrmBidBondPageReqVO pageReqVO) {
|
PageResult<CrmBidBondDO> page = bidBondMapper.selectPage(pageReqVO);
|
return new PageResult<>(buildRespList(page.getList()), page.getTotal());
|
}
|
|
@Override
|
public List<CrmBidBondSaleOrderRespVO> getSaleOrderList(Long customerId) {
|
if (customerId == null) {
|
return List.of();
|
}
|
List<ErpSaleOrderRespDTO> orders = erpSaleOrderApi.getSaleOrderListByCustomerId(customerId)
|
.getCheckedData();
|
if (CollUtil.isEmpty(orders)) {
|
return List.of();
|
}
|
return orders.stream()
|
.filter(order -> Objects.equals(ERP_SALE_ORDER_STATUS_APPROVE, order.getStatus()))
|
.sorted(Comparator.comparing(ErpSaleOrderRespDTO::getId).reversed())
|
.map(order -> {
|
CrmBidBondSaleOrderRespVO respVO = new CrmBidBondSaleOrderRespVO();
|
respVO.setId(order.getId());
|
respVO.setNo(order.getNo());
|
respVO.setCustomerId(order.getCustomerId());
|
respVO.setStatus(order.getStatus());
|
respVO.setStatusName(getSaleOrderStatusName(order.getStatus()));
|
respVO.setTotalPrice(order.getTotalPrice());
|
respVO.setContractNo(order.getContractNo());
|
return respVO;
|
})
|
.collect(Collectors.toList());
|
}
|
|
// ========== 校验与组装 ==========
|
|
private CrmBidBondDO validateAndBuild(CrmBidBondSaveReqVO reqVO) {
|
// 1. 客户必须存在
|
CrmCustomerDO customer = customerService.getCustomer(reqVO.getCustomerId());
|
if (customer == null) {
|
throw new ServiceException(ErrorCodeConstants.BID_BOND_CUSTOMER_NOT_EXISTS.getCode(),
|
"客户(ID=" + reqVO.getCustomerId() + ")不存在,请刷新客户列表后重新选择。");
|
}
|
// 2. 中标结果
|
if (CrmBidResultEnum.valueOf(reqVO.getBidResult()) == null) {
|
throw new ServiceException(ErrorCodeConstants.BID_BOND_BID_RESULT_INVALID.getCode(),
|
"中标结果值【" + reqVO.getBidResult() + "】不正确,可选 0待开标、1已中标、2未中标。请重新选择。");
|
}
|
Integer bidResult = reqVO.getBidResult();
|
// 3. 保证金金额
|
if (reqVO.getBondAmount() == null || reqVO.getBondAmount().compareTo(BigDecimal.ZERO) <= 0) {
|
throw new ServiceException(ErrorCodeConstants.BID_BOND_AMOUNT_INVALID.getCode(),
|
"保证金金额为【" + reqVO.getBondAmount() + "】,必须大于 0,请填写正确的缴纳金额。");
|
}
|
BigDecimal bondAmount = reqVO.getBondAmount();
|
|
// 4. 关联销售订单(销售台账)
|
String saleOrderNo = null;
|
if (reqVO.getSaleOrderId() != null) {
|
ErpSaleOrderRespDTO order = erpSaleOrderApi.getSaleOrder(reqVO.getSaleOrderId())
|
.getCheckedData();
|
if (order == null) {
|
throw new ServiceException(ErrorCodeConstants.BID_BOND_SALE_ORDER_NOT_EXISTS.getCode(),
|
"关联销售订单(ID=" + reqVO.getSaleOrderId() + ")不存在,请刷新销售台账后重新选择。");
|
}
|
if (!Objects.equals(ERP_SALE_ORDER_STATUS_APPROVE, order.getStatus())) {
|
throw new ServiceException(ErrorCodeConstants.BID_BOND_SALE_ORDER_NOT_APPROVE.getCode(),
|
"关联销售订单[" + order.getNo() + "]当前状态为【" + getSaleOrderStatusName(order.getStatus())
|
+ "】,只能关联「已审核」的销售订单。请先审核该订单,或改选其他已审核订单。");
|
}
|
if (!Objects.equals(reqVO.getCustomerId(), order.getCustomerId())) {
|
throw new ServiceException(ErrorCodeConstants.BID_BOND_SALE_ORDER_CUSTOMER_MISMATCH.getCode(),
|
"关联销售订单[" + order.getNo() + "]的客户(ID=" + order.getCustomerId()
|
+ ")与保证金客户(ID=" + reqVO.getCustomerId()
|
+ ")不一致。请改选该客户名下的销售订单,或先调整保证金客户。");
|
}
|
if (CrmBidResultEnum.LOST.getResult().equals(bidResult)) {
|
throw new ServiceException(ErrorCodeConstants.BID_BOND_LOST_CANNOT_LINK_ORDER.getCode(),
|
"中标结果为【未中标】时不能关联销售订单[" + order.getNo()
|
+ "]。如已中标,请先将中标结果改为【已中标】;如确未中标,请清空关联销售订单。");
|
}
|
// 选中销售订单说明已中标,待开标时自动置为已中标
|
if (CrmBidResultEnum.PENDING.getResult().equals(bidResult)) {
|
bidResult = CrmBidResultEnum.WON.getResult();
|
}
|
saleOrderNo = order.getNo();
|
}
|
|
// 5. 退回状态与金额
|
Integer refundStatus = resolveRefundStatus(reqVO.getRefundStatus(), bidResult);
|
validateRefund(reqVO, bondAmount, refundStatus);
|
|
CrmBidBondDO bond = BeanUtils.toBean(reqVO, CrmBidBondDO.class);
|
bond.setId(reqVO.getId());
|
bond.setBidResult(bidResult);
|
bond.setRefundStatus(refundStatus);
|
bond.setSaleOrderNo(saleOrderNo);
|
return bond;
|
}
|
|
private Integer resolveRefundStatus(Integer refundStatus, Integer bidResult) {
|
if (refundStatus != null) {
|
if (CrmBidBondRefundStatusEnum.valueOf(refundStatus) == null) {
|
throw new ServiceException(ErrorCodeConstants.BID_BOND_REFUND_STATUS_INVALID.getCode(),
|
"退回状态值【" + refundStatus + "】不正确,可选 0未退回、1部分退回、2已退回、3无需退回。请重新选择。");
|
}
|
return refundStatus;
|
}
|
if (CrmBidResultEnum.LOST.getResult().equals(bidResult)) {
|
return CrmBidBondRefundStatusEnum.NOT_REFUNDED.getStatus();
|
}
|
if (CrmBidResultEnum.WON.getResult().equals(bidResult)) {
|
return CrmBidBondRefundStatusEnum.NO_NEED.getStatus();
|
}
|
return CrmBidBondRefundStatusEnum.NOT_REFUNDED.getStatus();
|
}
|
|
private void validateRefund(CrmBidBondSaveReqVO reqVO, BigDecimal bondAmount, Integer refundStatus) {
|
BigDecimal refundAmount = reqVO.getRefundAmount();
|
if (refundAmount != null && (refundAmount.compareTo(BigDecimal.ZERO) <= 0
|
|| refundAmount.compareTo(bondAmount) > 0)) {
|
throw new ServiceException(ErrorCodeConstants.BID_BOND_REFUND_AMOUNT_INVALID.getCode(),
|
"退回金额为【" + refundAmount + "】,保证金金额为【" + bondAmount
|
+ "】,退回金额必须大于 0 且不能超过保证金金额,请修改后重新保存。");
|
}
|
if (CrmBidBondRefundStatusEnum.REFUNDED.getStatus().equals(refundStatus)) {
|
if (refundAmount == null) {
|
throw new ServiceException(ErrorCodeConstants.BID_BOND_REFUND_AMOUNT_INVALID.getCode(),
|
"退回状态为【已退回】时,退回金额不能为空,请填写实际退回金额。");
|
}
|
if (refundAmount.compareTo(bondAmount) != 0) {
|
throw new ServiceException(ErrorCodeConstants.BID_BOND_REFUND_AMOUNT_FULL_INVALID.getCode(),
|
"退回状态为【已退回】时,退回金额【" + refundAmount + "】必须等于保证金金额【" + bondAmount
|
+ "】。请补齐差额,或将退回状态改为【部分退回】。");
|
}
|
if (reqVO.getRefundDate() == null) {
|
throw new ServiceException(ErrorCodeConstants.BID_BOND_REFUND_DATE_REQUIRED.getCode(),
|
"退回状态为【已退回】时,退回日期不能为空,请填写实际退回日期。");
|
}
|
} else if (CrmBidBondRefundStatusEnum.PARTIAL_REFUNDED.getStatus().equals(refundStatus)) {
|
if (refundAmount == null) {
|
throw new ServiceException(ErrorCodeConstants.BID_BOND_REFUND_AMOUNT_INVALID.getCode(),
|
"退回状态为【部分退回】时,退回金额不能为空,请填写本次实际退回金额。");
|
}
|
if (refundAmount.compareTo(bondAmount) >= 0) {
|
throw new ServiceException(ErrorCodeConstants.BID_BOND_REFUND_AMOUNT_PARTIAL_INVALID.getCode(),
|
"退回状态为【部分退回】时,退回金额【" + refundAmount + "】必须小于保证金金额【" + bondAmount
|
+ "】。如已全额退回,请将退回状态改为【已退回】。");
|
}
|
if (reqVO.getRefundDate() == null) {
|
throw new ServiceException(ErrorCodeConstants.BID_BOND_REFUND_DATE_REQUIRED.getCode(),
|
"退回状态为【部分退回】时,退回日期不能为空,请填写本次实际退回日期。");
|
}
|
} else if (refundAmount != null) {
|
throw new ServiceException(ErrorCodeConstants.BID_BOND_REFUND_STATUS_AMOUNT_CONFLICT.getCode(),
|
"退回状态为【" + CrmBidBondRefundStatusEnum.valueOf(refundStatus).getName()
|
+ "】时,退回金额应为空。如需记录退款,请将退回状态改为【部分退回】或【已退回】。");
|
}
|
}
|
|
private CrmBidBondDO validateBidBondExists(Long id) {
|
CrmBidBondDO bond = bidBondMapper.selectById(id);
|
if (bond == null) {
|
throw ServiceExceptionUtil.exception(ErrorCodeConstants.BID_BOND_NOT_EXISTS);
|
}
|
return bond;
|
}
|
|
private String generateBondNo() {
|
String date = LocalDate.now().format(NO_DATE_FORMAT);
|
for (int attempt = 0; attempt < 20; attempt++) {
|
int seq = (int) (Math.random() * 9000) + 1000;
|
String no = "BZ" + date + String.format("%04d", seq);
|
if (bidBondMapper.selectByNo(no) == null) {
|
return no;
|
}
|
}
|
throw new ServiceException(ErrorCodeConstants.BID_BOND_NO_EXISTS.getCode(),
|
"保证金单号在多次尝试后仍重复,请稍后重试。");
|
}
|
|
// ========== 组装 ==========
|
|
private List<CrmBidBondRespVO> buildRespList(List<CrmBidBondDO> bonds) {
|
if (CollUtil.isEmpty(bonds)) {
|
return List.of();
|
}
|
Set<Long> customerIds = bonds.stream().map(CrmBidBondDO::getCustomerId)
|
.filter(Objects::nonNull).collect(Collectors.toSet());
|
Map<Long, CrmCustomerDO> customerMap = customerIds.isEmpty() ? Map.of()
|
: customerService.getCustomerMap(customerIds);
|
Map<Long, ErpSaleOrderRespDTO> orderMap = loadSaleOrderMap(bonds);
|
|
return bonds.stream().map(bond -> {
|
CrmBidBondRespVO respVO = BeanUtils.toBean(bond, CrmBidBondRespVO.class);
|
CrmCustomerDO customer = customerMap.get(bond.getCustomerId());
|
if (customer != null) {
|
respVO.setCustomerName(customer.getName());
|
}
|
ErpSaleOrderRespDTO order = orderMap.get(bond.getSaleOrderId());
|
if (order != null) {
|
respVO.setSaleOrderNo(order.getNo());
|
respVO.setSaleOrderAmount(order.getTotalPrice());
|
}
|
return respVO;
|
}).collect(Collectors.toList());
|
}
|
|
/**
|
* 按客户批量加载关联销售订单,避免逐行 RPC
|
*/
|
private Map<Long, ErpSaleOrderRespDTO> loadSaleOrderMap(List<CrmBidBondDO> bonds) {
|
Set<Long> customerIds = bonds.stream()
|
.filter(bond -> bond.getSaleOrderId() != null)
|
.map(CrmBidBondDO::getCustomerId)
|
.filter(Objects::nonNull)
|
.collect(Collectors.toSet());
|
if (customerIds.isEmpty()) {
|
return Map.of();
|
}
|
Map<Long, ErpSaleOrderRespDTO> orderMap = new HashMap<>();
|
for (Long customerId : customerIds) {
|
try {
|
List<ErpSaleOrderRespDTO> orders = erpSaleOrderApi.getSaleOrderListByCustomerId(customerId)
|
.getCheckedData();
|
if (CollUtil.isNotEmpty(orders)) {
|
orders.forEach(order -> orderMap.put(order.getId(), order));
|
}
|
} catch (Exception ex) {
|
log.warn("[loadSaleOrderMap] 获取客户({})的销售订单失败", customerId, ex);
|
}
|
}
|
return orderMap;
|
}
|
|
private String getSaleOrderStatusName(Integer status) {
|
if (status == null) {
|
return "未知";
|
}
|
return switch (status) {
|
case 0 -> "未提交";
|
case 10 -> "未审核";
|
case 20 -> "已审核";
|
case 35 -> "已驳回";
|
default -> "状态" + status;
|
};
|
}
|
|
}
|