liu
7 天以前 0240f4dceb273622ddf7ab577ab9018295c17233
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
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);
    }
 
}