package cn.iocoder.yudao.module.crm.service.salesTarget;
|
|
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.util.StrUtil;
|
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.salesTarget.vo.CrmSalesTargetChangeStatusReqVO;
|
import cn.iocoder.yudao.module.crm.controller.admin.salesTarget.vo.CrmSalesTargetPageReqVO;
|
import cn.iocoder.yudao.module.crm.controller.admin.salesTarget.vo.CrmSalesTargetRespVO;
|
import cn.iocoder.yudao.module.crm.controller.admin.salesTarget.vo.CrmSalesTargetSaveReqVO;
|
import cn.iocoder.yudao.module.crm.controller.admin.salesTarget.vo.analysis.CrmSalesTargetAnalysisReqVO;
|
import cn.iocoder.yudao.module.crm.dal.dataobject.salesTarget.CrmSalesTargetDO;
|
import cn.iocoder.yudao.module.crm.dal.mysql.salesTarget.CrmSalesTargetAchievementMapper;
|
import cn.iocoder.yudao.module.crm.dal.mysql.salesTarget.CrmSalesTargetMapper;
|
import cn.iocoder.yudao.module.crm.enums.salesTarget.CrmSalesTargetDimensionEnum;
|
import cn.iocoder.yudao.module.crm.enums.salesTarget.CrmSalesTargetPeriodTypeEnum;
|
import cn.iocoder.yudao.module.crm.enums.salesTarget.CrmSalesTargetStatusEnum;
|
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
|
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
|
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
|
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
|
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
|
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
|
import jakarta.annotation.Resource;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.validation.annotation.Validated;
|
|
import java.math.BigDecimal;
|
import java.math.RoundingMode;
|
import java.time.LocalDateTime;
|
import java.time.YearMonth;
|
import java.time.format.DateTimeFormatter;
|
import java.util.ArrayList;
|
import java.util.Collections;
|
import java.util.HashMap;
|
import java.util.HashSet;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Objects;
|
import java.util.Set;
|
import java.util.regex.Matcher;
|
import java.util.regex.Pattern;
|
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
|
import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
|
|
/**
|
* 销售目标 Service 实现类
|
*
|
* @author 超级管理员
|
*/
|
@Service
|
@Validated
|
public class CrmSalesTargetServiceImpl implements CrmSalesTargetService {
|
|
private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
|
|
private static final Pattern YEAR_PATTERN = Pattern.compile("^\\d{4}$");
|
private static final Pattern QUARTER_PATTERN = Pattern.compile("^(\\d{4})-Q([1-4])$");
|
private static final Pattern MONTH_PATTERN = Pattern.compile("^(\\d{4})-(0[1-9]|1[0-2])$");
|
private static final DateTimeFormatter MONTH_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM");
|
|
@Resource
|
private CrmSalesTargetMapper salesTargetMapper;
|
@Resource
|
private CrmSalesTargetAchievementMapper achievementMapper;
|
|
@Resource
|
private AdminUserApi adminUserApi;
|
@Resource
|
private DeptApi deptApi;
|
@Resource
|
private MdmItemApi mdmItemApi;
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public Long createSalesTarget(CrmSalesTargetSaveReqVO createReqVO) {
|
// 1. 校验目标维度对象
|
validateTargetDimension(createReqVO);
|
// 2. 计算周期起止,并插入
|
CrmSalesTargetDO salesTarget = BeanUtils.toBean(createReqVO, CrmSalesTargetDO.class);
|
LocalDateTime[] period = calculatePeriod(createReqVO.getPeriodType(), createReqVO.getPeriodValue());
|
salesTarget.setStartTime(period[0]).setEndTime(period[1])
|
.setStatus(calcStatusByPeriod(period[0], period[1]));
|
salesTargetMapper.insert(salesTarget);
|
return salesTarget.getId();
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void updateSalesTarget(CrmSalesTargetSaveReqVO updateReqVO) {
|
// 1. 校验存在
|
CrmSalesTargetDO salesTarget = validateSalesTargetExists(updateReqVO.getId());
|
// 2. 校验目标维度对象
|
validateTargetDimension(updateReqVO);
|
// 3. 更新
|
CrmSalesTargetDO updateDO = BeanUtils.toBean(updateReqVO, CrmSalesTargetDO.class);
|
LocalDateTime[] period = calculatePeriod(updateReqVO.getPeriodType(), updateReqVO.getPeriodValue());
|
updateDO.setStartTime(period[0]).setEndTime(period[1]);
|
// 手动作废的状态不因编辑而覆盖
|
updateDO.setStatus(Objects.equals(salesTarget.getStatus(), CrmSalesTargetStatusEnum.INVALID.getStatus())
|
? CrmSalesTargetStatusEnum.INVALID.getStatus() : calcStatusByPeriod(period[0], period[1]));
|
salesTargetMapper.updateById(updateDO);
|
}
|
|
@Override
|
public void changeSalesTargetStatus(CrmSalesTargetChangeStatusReqVO reqVO) {
|
// 1. 校验存在
|
validateSalesTargetExists(reqVO.getId());
|
// 2. 更新状态
|
salesTargetMapper.updateById(new CrmSalesTargetDO().setId(reqVO.getId()).setStatus(reqVO.getStatus()));
|
}
|
|
@Override
|
public void deleteSalesTarget(Long id) {
|
// 1. 校验存在
|
validateSalesTargetExists(id);
|
// 2. 删除
|
salesTargetMapper.deleteById(id);
|
}
|
|
@Override
|
public CrmSalesTargetRespVO getSalesTarget(Long id) {
|
CrmSalesTargetDO salesTarget = validateSalesTargetExists(id);
|
return convertListToRespVO(Collections.singletonList(salesTarget)).get(0);
|
}
|
|
@Override
|
public PageResult<CrmSalesTargetRespVO> getSalesTargetPage(CrmSalesTargetPageReqVO pageReqVO) {
|
PageResult<CrmSalesTargetDO> pageResult = salesTargetMapper.selectPage(pageReqVO);
|
if (CollUtil.isEmpty(pageResult.getList())) {
|
return PageResult.empty();
|
}
|
return new PageResult<>(convertListToRespVO(pageResult.getList()), pageResult.getTotal());
|
}
|
|
@Override
|
public List<CrmSalesTargetRespVO> getSalesTargetList(CrmSalesTargetPageReqVO reqVO) {
|
List<CrmSalesTargetDO> list = salesTargetMapper.selectList(reqVO);
|
return convertListToRespVO(list);
|
}
|
|
@Override
|
public List<CrmSalesTargetRespVO> getSalesTargetAnalysis(CrmSalesTargetAnalysisReqVO reqVO) {
|
// 按周期 + 维度过滤查询全部目标(含已结束),用于达成分析看板
|
List<CrmSalesTargetDO> list = salesTargetMapper.selectList(new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX<CrmSalesTargetDO>()
|
.eqIfPresent(CrmSalesTargetDO::getStatus, reqVO.getStatus())
|
.eqIfPresent(CrmSalesTargetDO::getTargetDimension, reqVO.getTargetDimension())
|
.eq(CrmSalesTargetDO::getPeriodType, reqVO.getPeriodType())
|
.eq(CrmSalesTargetDO::getPeriodValue, reqVO.getPeriodValue())
|
.orderByDesc(CrmSalesTargetDO::getId));
|
return convertListToRespVO(list);
|
}
|
|
// ========== 私有方法 ==========
|
|
private CrmSalesTargetDO validateSalesTargetExists(Long id) {
|
CrmSalesTargetDO salesTarget = salesTargetMapper.selectById(id);
|
if (salesTarget == null) {
|
throw exception(CRM_SALES_TARGET_NOT_EXISTS);
|
}
|
return salesTarget;
|
}
|
|
private void validateTargetDimension(CrmSalesTargetSaveReqVO reqVO) {
|
if (Objects.equals(reqVO.getTargetDimension(), CrmSalesTargetDimensionEnum.USER.getType())
|
&& reqVO.getTargetUserId() == null) {
|
throw exception(CRM_SALES_TARGET_DIMENSION_TARGET_MISSING, "用户");
|
}
|
if (Objects.equals(reqVO.getTargetDimension(), CrmSalesTargetDimensionEnum.DEPT.getType())
|
&& reqVO.getTargetDeptId() == null) {
|
throw exception(CRM_SALES_TARGET_DIMENSION_TARGET_MISSING, "部门");
|
}
|
if (Objects.equals(reqVO.getTargetDimension(), CrmSalesTargetDimensionEnum.ITEM.getType())
|
&& reqVO.getTargetItemId() == null) {
|
throw exception(CRM_SALES_TARGET_DIMENSION_TARGET_MISSING, "物料");
|
}
|
}
|
|
/**
|
* 根据周期类型 + 周期值计算周期起止时间(半开区间 [start, end))
|
*/
|
private LocalDateTime[] calculatePeriod(Integer periodType, String periodValue) {
|
if (Objects.equals(periodType, CrmSalesTargetPeriodTypeEnum.YEAR.getType())) {
|
if (!YEAR_PATTERN.matcher(periodValue).matches()) {
|
throw exception(CRM_SALES_TARGET_PERIOD_VALUE_INVALID, periodValue, "2026");
|
}
|
int year = Integer.parseInt(periodValue);
|
return new LocalDateTime[]{LocalDateTime.of(year, 1, 1, 0, 0), LocalDateTime.of(year + 1, 1, 1, 0, 0)};
|
}
|
if (Objects.equals(periodType, CrmSalesTargetPeriodTypeEnum.QUARTER.getType())) {
|
Matcher matcher = QUARTER_PATTERN.matcher(periodValue);
|
if (!matcher.matches()) {
|
throw exception(CRM_SALES_TARGET_PERIOD_VALUE_INVALID, periodValue, "2026-Q1");
|
}
|
int year = Integer.parseInt(matcher.group(1));
|
int startMonth = (Integer.parseInt(matcher.group(2)) - 1) * 3 + 1;
|
return new LocalDateTime[]{
|
YearMonth.of(year, startMonth).atDay(1).atStartOfDay(),
|
YearMonth.of(year, startMonth).plusMonths(3).atDay(1).atStartOfDay()};
|
}
|
if (Objects.equals(periodType, CrmSalesTargetPeriodTypeEnum.MONTH.getType())) {
|
if (!MONTH_PATTERN.matcher(periodValue).matches()) {
|
throw exception(CRM_SALES_TARGET_PERIOD_VALUE_INVALID, periodValue, "2026-07");
|
}
|
YearMonth yearMonth = YearMonth.parse(periodValue, MONTH_FORMAT);
|
return new LocalDateTime[]{
|
yearMonth.atDay(1).atStartOfDay(),
|
yearMonth.plusMonths(1).atDay(1).atStartOfDay()};
|
}
|
throw exception(CRM_SALES_TARGET_PERIOD_VALUE_INVALID, periodValue, "年/季度/月");
|
}
|
|
/**
|
* 根据当前时间与周期起止,自动判定目标状态
|
*/
|
private Integer calcStatusByPeriod(LocalDateTime startTime, LocalDateTime endTime) {
|
LocalDateTime now = LocalDateTime.now();
|
if (now.isBefore(startTime)) {
|
return CrmSalesTargetStatusEnum.NOT_ACTIVE.getStatus();
|
}
|
if (now.isAfter(endTime)) {
|
return CrmSalesTargetStatusEnum.END.getStatus();
|
}
|
return CrmSalesTargetStatusEnum.ACTIVE.getStatus();
|
}
|
|
/**
|
* 将 DO 列表转换为 RespVO 列表,填充对象名称、创建人名称与实际达成数据
|
*/
|
private List<CrmSalesTargetRespVO> convertListToRespVO(List<CrmSalesTargetDO> targetList) {
|
if (CollUtil.isEmpty(targetList)) {
|
return Collections.emptyList();
|
}
|
// 1. 收集关联 ID,批量查询名称
|
Set<Long> userIds = new HashSet<>();
|
Set<Long> deptIds = new HashSet<>();
|
Set<Long> itemIds = new HashSet<>();
|
for (CrmSalesTargetDO target : targetList) {
|
if (Objects.equals(target.getTargetDimension(), CrmSalesTargetDimensionEnum.USER.getType())) {
|
userIds.add(target.getTargetUserId());
|
} else if (Objects.equals(target.getTargetDimension(), CrmSalesTargetDimensionEnum.DEPT.getType())) {
|
deptIds.add(target.getTargetDeptId());
|
} else if (Objects.equals(target.getTargetDimension(), CrmSalesTargetDimensionEnum.ITEM.getType())) {
|
itemIds.add(target.getTargetItemId());
|
}
|
if (StrUtil.isNumeric(target.getCreator())) { // 创建人(用户编号字符串)
|
userIds.add(Long.valueOf(target.getCreator()));
|
}
|
}
|
Map<Long, AdminUserRespDTO> userMap = CollUtil.isNotEmpty(userIds) ? adminUserApi.getUserMap(userIds) : Collections.emptyMap();
|
Map<Long, DeptRespDTO> deptMap = CollUtil.isNotEmpty(deptIds) ? deptApi.getDeptMap(deptIds) : Collections.emptyMap();
|
Map<Long, MdmItemRespDTO> itemMap = CollUtil.isNotEmpty(itemIds) ? mdmItemApi.getItemMap(itemIds) : Collections.emptyMap();
|
|
// 2. 转换并填充
|
return convertList(targetList, target -> {
|
CrmSalesTargetRespVO respVO = BeanUtils.toBean(target, CrmSalesTargetRespVO.class);
|
// 目标对象名称
|
if (Objects.equals(target.getTargetDimension(), CrmSalesTargetDimensionEnum.USER.getType())) {
|
AdminUserRespDTO user = userMap.get(target.getTargetUserId());
|
if (user != null) {
|
respVO.setTargetUserName(user.getNickname());
|
}
|
} else if (Objects.equals(target.getTargetDimension(), CrmSalesTargetDimensionEnum.DEPT.getType())) {
|
DeptRespDTO dept = deptMap.get(target.getTargetDeptId());
|
if (dept != null) {
|
respVO.setTargetDeptName(dept.getName());
|
}
|
} else if (Objects.equals(target.getTargetDimension(), CrmSalesTargetDimensionEnum.ITEM.getType())) {
|
MdmItemRespDTO item = itemMap.get(target.getTargetItemId());
|
if (item != null) {
|
respVO.setTargetItemName(item.getName());
|
}
|
}
|
// 创建人名称
|
if (StrUtil.isNumeric(target.getCreator())) {
|
AdminUserRespDTO creator = userMap.get(Long.valueOf(target.getCreator()));
|
if (creator != null) {
|
respVO.setCreatorName(creator.getNickname());
|
}
|
}
|
// 实际达成与达成率
|
fillAchievement(target, respVO);
|
return respVO;
|
});
|
}
|
|
/**
|
* 计算实际达成金额/数量与达成率
|
*/
|
private void fillAchievement(CrmSalesTargetDO target, CrmSalesTargetRespVO respVO) {
|
if (Objects.equals(target.getTargetDimension(), CrmSalesTargetDimensionEnum.ITEM.getType())) {
|
// 产品维度:仅合同金额与合同数可按物料统计
|
BigDecimal contractPrice = achievementMapper.selectContractPriceByItemId(target.getTargetItemId(),
|
target.getStartTime(), target.getEndTime());
|
Long contractCount = achievementMapper.selectContractCountByItemId(target.getTargetItemId(),
|
target.getStartTime(), target.getEndTime());
|
respVO.setActualContractPrice(contractPrice);
|
respVO.setActualContractCount(contractCount);
|
respVO.setActualReceivablePrice(BigDecimal.ZERO);
|
respVO.setActualBusinessPrice(BigDecimal.ZERO);
|
respVO.setActualCustomerCount(0L);
|
} else {
|
Set<Long> userIds = resolveTargetUserIds(target);
|
if (CollUtil.isEmpty(userIds)) { // 部门下无用户
|
respVO.setActualContractPrice(BigDecimal.ZERO);
|
respVO.setActualContractCount(0L);
|
respVO.setActualReceivablePrice(BigDecimal.ZERO);
|
respVO.setActualBusinessPrice(BigDecimal.ZERO);
|
respVO.setActualCustomerCount(0L);
|
} else {
|
respVO.setActualContractPrice(achievementMapper.selectContractPriceByUserIds(userIds,
|
target.getStartTime(), target.getEndTime()));
|
respVO.setActualContractCount(achievementMapper.selectContractCountByUserIds(userIds,
|
target.getStartTime(), target.getEndTime()));
|
respVO.setActualReceivablePrice(achievementMapper.selectReceivablePriceByUserIds(userIds,
|
target.getStartTime(), target.getEndTime()));
|
respVO.setActualBusinessPrice(achievementMapper.selectBusinessPriceByUserIds(userIds,
|
target.getStartTime(), target.getEndTime()));
|
respVO.setActualCustomerCount(achievementMapper.selectCustomerCountByUserIds(userIds,
|
target.getStartTime(), target.getEndTime()));
|
}
|
}
|
// 达成率 = 实际 / 目标 * 100
|
respVO.setContractRate(calcRate(respVO.getActualContractPrice(), target.getContractTarget()));
|
respVO.setReceivableRate(calcRate(respVO.getActualReceivablePrice(), target.getReceivableTarget()));
|
respVO.setBusinessRate(calcRate(respVO.getActualBusinessPrice(), target.getBusinessTarget()));
|
respVO.setCustomerRate(calcRate(respVO.getActualCustomerCount(), target.getCustomerTarget()));
|
respVO.setContractCountRate(calcRate(respVO.getActualContractCount(), target.getContractCountTarget()));
|
}
|
|
/**
|
* 解析目标的统计用户集合:个人维度为单用户,部门维度为部门及子部门下所有用户
|
*/
|
private Set<Long> resolveTargetUserIds(CrmSalesTargetDO target) {
|
if (Objects.equals(target.getTargetDimension(), CrmSalesTargetDimensionEnum.USER.getType())) {
|
return target.getTargetUserId() == null ? Collections.emptySet() : Collections.singleton(target.getTargetUserId());
|
}
|
// 部门维度:含子部门
|
List<Long> deptIds = new ArrayList<>();
|
deptIds.add(target.getTargetDeptId());
|
List<DeptRespDTO> childDepts = deptApi.getChildDeptList(target.getTargetDeptId());
|
if (CollUtil.isNotEmpty(childDepts)) {
|
deptIds.addAll(convertList(childDepts, DeptRespDTO::getId));
|
}
|
return convertSet(adminUserApi.getUserListByDeptIds(deptIds), AdminUserRespDTO::getId);
|
}
|
|
private BigDecimal calcRate(BigDecimal actual, BigDecimal target) {
|
if (target == null || target.compareTo(BigDecimal.ZERO) <= 0) {
|
return null;
|
}
|
return (actual == null ? BigDecimal.ZERO : actual)
|
.multiply(HUNDRED).divide(target, 2, RoundingMode.HALF_UP);
|
}
|
|
private BigDecimal calcRate(Long actual, Integer target) {
|
if (target == null || target <= 0) {
|
return null;
|
}
|
return BigDecimal.valueOf(actual == null ? 0 : actual)
|
.multiply(HUNDRED).divide(BigDecimal.valueOf(target), 2, RoundingMode.HALF_UP);
|
}
|
|
}
|