Commit 6ba73e60f0ddfad5f6d4446f477871fc47108921

Authored by YunaiV
2 parents 9163ede8 b0b80676

Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/db

Showing 60 changed files with 966 additions and 213 deletions
yudao-dependencies/pom.xml
... ... @@ -31,7 +31,7 @@
31 31 <redisson.version>3.26.0</redisson.version>
32 32 <dm8.jdbc.version>8.1.3.62</dm8.jdbc.version>
33 33 <!-- 消息队列 -->
34   - <rocketmq-spring.version>2.2.3</rocketmq-spring.version>
  34 + <rocketmq-spring.version>2.3.0</rocketmq-spring.version>
35 35 <!-- 服务保障相关 -->
36 36 <lock4j.version>2.2.7</lock4j.version>
37 37 <!-- 监控相关 -->
... ...
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
... ... @@ -97,6 +97,10 @@ public class CollectionUtils {
97 97 .collect(Collectors.toList());
98 98 }
99 99  
  100 + public static <T> Set<T> convertSet(Collection<T> from) {
  101 + return convertSet(from, v -> v);
  102 + }
  103 +
100 104 public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func) {
101 105 if (CollUtil.isEmpty(from)) {
102 106 return new HashSet<>();
... ...
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/rabbitmq/config/YudaoRabbitMQAutoConfiguration.java
1 1 package cn.iocoder.yudao.framework.mq.rabbitmq.config;
2 2  
3   -import cn.hutool.core.util.ReflectUtil;
4 3 import lombok.extern.slf4j.Slf4j;
5   -import org.springframework.amqp.utils.SerializationUtils;
  4 +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
  5 +import org.springframework.amqp.support.converter.MessageConverter;
6 6 import org.springframework.boot.autoconfigure.AutoConfiguration;
7 7 import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
8   -
9   -import java.lang.reflect.Field;
  8 +import org.springframework.context.annotation.Bean;
10 9  
11 10 /**
12 11 * RabbitMQ 消息队列配置类
... ... @@ -18,12 +17,12 @@ import java.lang.reflect.Field;
18 17 @ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate")
19 18 public class YudaoRabbitMQAutoConfiguration {
20 19  
21   - static {
22   - // 强制设置 SerializationUtils 的 TRUST_ALL 为 true,避免 RabbitMQ Consumer 反序列化消息报错
23   - // 为什么不通过设置 spring.amqp.deserialization.trust.all 呢?因为可能在 SerializationUtils static 初始化后
24   - Field trustAllField = ReflectUtil.getField(SerializationUtils.class, "TRUST_ALL");
25   - ReflectUtil.removeFinalModify(trustAllField);
26   - ReflectUtil.setFieldValue(SerializationUtils.class, trustAllField, true);
  20 + /**
  21 + * Jackson2JsonMessageConverter Bean:使用 jackson 序列化消息
  22 + */
  23 + @Bean
  24 + public MessageConverter createMessageConverter() {
  25 + return new Jackson2JsonMessageConverter();
27 26 }
28 27  
29 28 }
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
1   -package cn.iocoder.yudao.module.bpm.service.task; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.date.DateUtils; import cn.iocoder.yudao.framework.common.util.object.PageUtils; import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCancelReqVO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCreateReqVO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstancePageReqVO; import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert; import cn.iocoder.yudao.module.bpm.enums.task.BpmDeleteReasonEnum; import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.BpmTaskCandidateStartUserSelectStrategy; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmConstants; import cn.iocoder.yudao.module.bpm.framework.flowable.core.event.BpmProcessInstanceEventPublisher; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import jakarta.annotation.Resource; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import org.flowable.bpmn.model.BpmnModel; import org.flowable.bpmn.model.UserTask; import org.flowable.engine.HistoryService; import org.flowable.engine.RuntimeService; import org.flowable.engine.delegate.event.FlowableCancelledEvent; import org.flowable.engine.history.HistoricProcessInstance; import org.flowable.engine.history.HistoricProcessInstanceQuery; import org.flowable.engine.repository.ProcessDefinition; import org.flowable.engine.runtime.ProcessInstance; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; /** * 流程实例 Service 实现类 * * ProcessDefinition & ProcessInstance & Execution & Task 的关系: * 1. <a href="https://blog.csdn.net/bobozai86/article/details/105210414" /> * * HistoricProcessInstance & ProcessInstance 的关系: * 1. <a href=" https://my.oschina.net/843294669/blog/71902" /> * * 简单来说,前者 = 历史 + 运行中的流程实例,后者仅是运行中的流程实例 * * @author 芋道源码 */ @Service @Validated @Slf4j public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService { @Resource private RuntimeService runtimeService; @Resource private HistoryService historyService; @Resource private BpmProcessDefinitionService processDefinitionService; @Resource private BpmMessageService messageService; @Resource private AdminUserApi adminUserApi; @Resource private BpmProcessInstanceEventPublisher processInstanceEventPublisher; @Override public ProcessInstance getProcessInstance(String id) { return runtimeService.createProcessInstanceQuery() .includeProcessVariables() .processInstanceId(id) .singleResult(); } @Override public List<ProcessInstance> getProcessInstances(Set<String> ids) { return runtimeService.createProcessInstanceQuery().processInstanceIds(ids).list(); } @Override public HistoricProcessInstance getHistoricProcessInstance(String id) { return historyService.createHistoricProcessInstanceQuery().processInstanceId(id).includeProcessVariables().singleResult(); } @Override public List<HistoricProcessInstance> getHistoricProcessInstances(Set<String> ids) { return historyService.createHistoricProcessInstanceQuery().processInstanceIds(ids).list(); } @Override public PageResult<HistoricProcessInstance> getProcessInstancePage(Long userId, BpmProcessInstancePageReqVO pageReqVO) { // 通过 BpmProcessInstanceExtDO 表,先查询到对应的分页 HistoricProcessInstanceQuery processInstanceQuery = historyService.createHistoricProcessInstanceQuery() .includeProcessVariables() .processInstanceTenantId(FlowableUtils.getTenantId()) .orderByProcessInstanceStartTime().desc(); if (userId != null) { // 【我的流程】菜单时,需要传递该字段 processInstanceQuery.startedBy(String.valueOf(userId)); } else if (pageReqVO.getStartUserId() != null) { // 【管理流程】菜单时,才会传递该字段 processInstanceQuery.startedBy(String.valueOf(pageReqVO.getStartUserId())); } if (StrUtil.isNotEmpty(pageReqVO.getName())) { processInstanceQuery.processInstanceNameLike("%" + pageReqVO.getName() + "%"); } if (StrUtil.isNotEmpty(pageReqVO.getProcessDefinitionId())) { processInstanceQuery.processDefinitionId("%" + pageReqVO.getProcessDefinitionId() + "%"); } if (StrUtil.isNotEmpty(pageReqVO.getCategory())) { processInstanceQuery.processDefinitionCategory(pageReqVO.getCategory()); } if (pageReqVO.getStatus() != null) { processInstanceQuery.variableValueEquals(BpmConstants.PROCESS_INSTANCE_VARIABLE_STATUS, pageReqVO.getStatus()); } if (ArrayUtil.isNotEmpty(pageReqVO.getCreateTime())) { processInstanceQuery.startedAfter(DateUtils.of(pageReqVO.getCreateTime()[0])); processInstanceQuery.startedBefore(DateUtils.of(pageReqVO.getCreateTime()[1])); } // 查询数量 long processInstanceCount = processInstanceQuery.count(); if (processInstanceCount == 0) { return PageResult.empty(processInstanceCount); } // 查询列表 List<HistoricProcessInstance> processInstanceList = processInstanceQuery.listPage(PageUtils.getStart(pageReqVO), pageReqVO.getPageSize()); return new PageResult<>(processInstanceList, processInstanceCount); } @Override @Transactional(rollbackFor = Exception.class) public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqVO createReqVO) { // 获得流程定义 ProcessDefinition definition = processDefinitionService.getProcessDefinition(createReqVO.getProcessDefinitionId()); // 发起流程 return createProcessInstance0(userId, definition, createReqVO.getVariables(), null, createReqVO.getStartUserSelectAssignees()); } @Override public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO createReqDTO) { // 获得流程定义 ProcessDefinition definition = processDefinitionService.getActiveProcessDefinition(createReqDTO.getProcessDefinitionKey()); // 发起流程 return createProcessInstance0(userId, definition, createReqDTO.getVariables(), createReqDTO.getBusinessKey(), createReqDTO.getStartUserSelectAssignees()); } private String createProcessInstance0(Long userId, ProcessDefinition definition, Map<String, Object> variables, String businessKey, Map<String, List<Long>> startUserSelectAssignees) { // 1.1 校验流程定义 if (definition == null) { throw exception(PROCESS_DEFINITION_NOT_EXISTS); } if (definition.isSuspended()) { throw exception(PROCESS_DEFINITION_IS_SUSPENDED); } // 1.2 校验发起人自选审批人 validateStartUserSelectAssignees(definition, startUserSelectAssignees); // 2. 创建流程实例 FlowableUtils.filterProcessInstanceFormVariable(variables); // 过滤一下,避免 ProcessInstance 系统级的变量被占用 variables.put(BpmConstants.PROCESS_INSTANCE_VARIABLE_STATUS, // 流程实例状态:审批中 BpmProcessInstanceStatusEnum.RUNNING.getStatus()); if (CollUtil.isNotEmpty(startUserSelectAssignees)) { variables.put(BpmConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, startUserSelectAssignees); } ProcessInstance instance = runtimeService.createProcessInstanceBuilder() .processDefinitionId(definition.getId()) .businessKey(businessKey) .name(definition.getName().trim()) .variables(variables) .start(); return instance.getId(); } private void validateStartUserSelectAssignees(ProcessDefinition definition, Map<String, List<Long>> startUserSelectAssignees) { // 1. 获得发起人自选审批人的 UserTask 列表 BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(definition.getId()); List<UserTask> userTaskList = BpmTaskCandidateStartUserSelectStrategy.getStartUserSelectUserTaskList(bpmnModel); if (CollUtil.isEmpty(userTaskList)) { return; } // 2. 校验发起人自选审批人的 UserTask 是否都配置了 userTaskList.forEach(userTask -> { List<Long> assignees = startUserSelectAssignees != null ? startUserSelectAssignees.get(userTask.getId()) : null; if (CollUtil.isEmpty(assignees)) { throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, userTask.getName()); } Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(assignees); assignees.forEach(assignee -> { if (userMap.get(assignee) == null) { throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_EXISTS, userTask.getName(), assignee); } }); }); } @Override public void cancelProcessInstanceByStartUser(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO) { // 1.1 校验流程实例存在 ProcessInstance instance = getProcessInstance(cancelReqVO.getId()); if (instance == null) { throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS); } // 1.2 只能取消自己的 if (!Objects.equals(instance.getStartUserId(), String.valueOf(userId))) { throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF); } // 2. 通过删除流程实例,实现流程实例的取消, // 删除流程实例,正则执行任务 ACT_RU_TASK. 任务会被删除。 deleteProcessInstance(cancelReqVO.getId(), BpmDeleteReasonEnum.CANCEL_PROCESS_INSTANCE_BY_START_USER.format(cancelReqVO.getReason())); // 3. 进一步的处理,交给 updateProcessInstanceCancel 方法 } @Override public void cancelProcessInstanceByAdmin(Long userId, BpmProcessInstanceCancelReqVO cancelReqVO) { // 1.1 校验流程实例存在 ProcessInstance instance = getProcessInstance(cancelReqVO.getId()); if (instance == null) { throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS); } // 1.2 管理员取消,不用校验是否为自己的 AdminUserRespDTO user = adminUserApi.getUser(userId); // 2. 通过删除流程实例,实现流程实例的取消, // 删除流程实例,正则执行任务 ACT_RU_TASK. 任务会被删除。 deleteProcessInstance(cancelReqVO.getId(), BpmDeleteReasonEnum.CANCEL_PROCESS_INSTANCE_BY_ADMIN.format(user.getNickname(), cancelReqVO.getReason())); // 3. 进一步的处理,交给 updateProcessInstanceCancel 方法 } @Override public void updateProcessInstanceWhenCancel(FlowableCancelledEvent event) { // 1. 判断是否为 Reject 不通过。如果是,则不进行更新. // 因为,updateProcessInstanceReject 方法(审批不通过),已经进行更新了 if (BpmDeleteReasonEnum.isRejectReason((String) event.getCause())) { return; } // 2. 更新流程实例 status runtimeService.setVariable(event.getProcessInstanceId(), BpmConstants.PROCESS_INSTANCE_VARIABLE_STATUS, BpmProcessInstanceStatusEnum.CANCEL.getStatus()); // 3. 发送流程实例的状态事件 // 注意:此时如果去查询 ProcessInstance 的话,字段是不全的,所以去查询了 HistoricProcessInstance HistoricProcessInstance processInstance = getHistoricProcessInstance(event.getProcessInstanceId()); // 发送流程实例的状态事件 processInstanceEventPublisher.sendProcessInstanceResultEvent( BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceStatusEvent(this, processInstance, BpmProcessInstanceStatusEnum.CANCEL.getStatus())); } @Override public void updateProcessInstanceWhenApprove(ProcessInstance instance) { // 1. 更新流程实例 status runtimeService.setVariable(instance.getId(), BpmConstants.PROCESS_INSTANCE_VARIABLE_STATUS, BpmProcessInstanceStatusEnum.APPROVE.getStatus()); // 2. 发送流程被【通过】的消息 messageService.sendMessageWhenProcessInstanceApprove(BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceApproveMessage(instance)); // 3. 发送流程实例的状态事件 // 注意:此时如果去查询 ProcessInstance 的话,字段是不全的,所以去查询了 HistoricProcessInstance HistoricProcessInstance processInstance = getHistoricProcessInstance(instance.getId()); processInstanceEventPublisher.sendProcessInstanceResultEvent( BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceStatusEvent(this, processInstance, BpmProcessInstanceStatusEnum.APPROVE.getStatus())); } @Override @Transactional(rollbackFor = Exception.class) public void updateProcessInstanceReject(String id, String reason) { // 1. 更新流程实例 status runtimeService.setVariable(id, BpmConstants.PROCESS_INSTANCE_VARIABLE_STATUS, BpmProcessInstanceStatusEnum.REJECT.getStatus()); // 2. 删除流程实例,以实现驳回任务时,取消整个审批流程 ProcessInstance processInstance = getProcessInstance(id); deleteProcessInstance(id, StrUtil.format(BpmDeleteReasonEnum.REJECT_TASK.format(reason))); // 3. 发送流程被【不通过】的消息 messageService.sendMessageWhenProcessInstanceReject(BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceRejectMessage(processInstance, reason)); // 4. 发送流程实例的状态事件 processInstanceEventPublisher.sendProcessInstanceResultEvent( BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceStatusEvent(this, processInstance, BpmProcessInstanceStatusEnum.REJECT.getStatus())); } private void deleteProcessInstance(String id, String reason) { runtimeService.deleteProcessInstance(id, reason); } }
2 1 \ No newline at end of file
  2 +package cn.iocoder.yudao.module.bpm.service.task; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.date.DateUtils; import cn.iocoder.yudao.framework.common.util.object.PageUtils; import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCancelReqVO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCreateReqVO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstancePageReqVO; import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert; import cn.iocoder.yudao.module.bpm.enums.task.BpmDeleteReasonEnum; import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.BpmTaskCandidateStartUserSelectStrategy; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmConstants; import cn.iocoder.yudao.module.bpm.framework.flowable.core.event.BpmProcessInstanceEventPublisher; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import jakarta.annotation.Resource; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import org.flowable.bpmn.model.BpmnModel; import org.flowable.bpmn.model.UserTask; import org.flowable.engine.HistoryService; import org.flowable.engine.RuntimeService; import org.flowable.engine.delegate.event.FlowableCancelledEvent; import org.flowable.engine.history.HistoricProcessInstance; import org.flowable.engine.history.HistoricProcessInstanceQuery; import org.flowable.engine.repository.ProcessDefinition; import org.flowable.engine.runtime.ProcessInstance; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.util.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; /** * 流程实例 Service 实现类 * * ProcessDefinition & ProcessInstance & Execution & Task 的关系: * 1. <a href="https://blog.csdn.net/bobozai86/article/details/105210414" /> * * HistoricProcessInstance & ProcessInstance 的关系: * 1. <a href=" https://my.oschina.net/843294669/blog/71902" /> * * 简单来说,前者 = 历史 + 运行中的流程实例,后者仅是运行中的流程实例 * * @author 芋道源码 */ @Service @Validated @Slf4j public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService { @Resource private RuntimeService runtimeService; @Resource private HistoryService historyService; @Resource private BpmProcessDefinitionService processDefinitionService; @Resource private BpmMessageService messageService; @Resource private AdminUserApi adminUserApi; @Resource private BpmProcessInstanceEventPublisher processInstanceEventPublisher; @Override public ProcessInstance getProcessInstance(String id) { return runtimeService.createProcessInstanceQuery() .includeProcessVariables() .processInstanceId(id) .singleResult(); } @Override public List<ProcessInstance> getProcessInstances(Set<String> ids) { return runtimeService.createProcessInstanceQuery().processInstanceIds(ids).list(); } @Override public HistoricProcessInstance getHistoricProcessInstance(String id) { return historyService.createHistoricProcessInstanceQuery().processInstanceId(id).includeProcessVariables().singleResult(); } @Override public List<HistoricProcessInstance> getHistoricProcessInstances(Set<String> ids) { return historyService.createHistoricProcessInstanceQuery().processInstanceIds(ids).list(); } @Override public PageResult<HistoricProcessInstance> getProcessInstancePage(Long userId, BpmProcessInstancePageReqVO pageReqVO) { // 通过 BpmProcessInstanceExtDO 表,先查询到对应的分页 HistoricProcessInstanceQuery processInstanceQuery = historyService.createHistoricProcessInstanceQuery() .includeProcessVariables() .processInstanceTenantId(FlowableUtils.getTenantId()) .orderByProcessInstanceStartTime().desc(); if (userId != null) { // 【我的流程】菜单时,需要传递该字段 processInstanceQuery.startedBy(String.valueOf(userId)); } else if (pageReqVO.getStartUserId() != null) { // 【管理流程】菜单时,才会传递该字段 processInstanceQuery.startedBy(String.valueOf(pageReqVO.getStartUserId())); } if (StrUtil.isNotEmpty(pageReqVO.getName())) { processInstanceQuery.processInstanceNameLike("%" + pageReqVO.getName() + "%"); } if (StrUtil.isNotEmpty(pageReqVO.getProcessDefinitionId())) { processInstanceQuery.processDefinitionId("%" + pageReqVO.getProcessDefinitionId() + "%"); } if (StrUtil.isNotEmpty(pageReqVO.getCategory())) { processInstanceQuery.processDefinitionCategory(pageReqVO.getCategory()); } if (pageReqVO.getStatus() != null) { processInstanceQuery.variableValueEquals(BpmConstants.PROCESS_INSTANCE_VARIABLE_STATUS, pageReqVO.getStatus()); } if (ArrayUtil.isNotEmpty(pageReqVO.getCreateTime())) { processInstanceQuery.startedAfter(DateUtils.of(pageReqVO.getCreateTime()[0])); processInstanceQuery.startedBefore(DateUtils.of(pageReqVO.getCreateTime()[1])); } // 查询数量 long processInstanceCount = processInstanceQuery.count(); if (processInstanceCount == 0) { return PageResult.empty(processInstanceCount); } // 查询列表 List<HistoricProcessInstance> processInstanceList = processInstanceQuery.listPage(PageUtils.getStart(pageReqVO), pageReqVO.getPageSize()); return new PageResult<>(processInstanceList, processInstanceCount); } @Override @Transactional(rollbackFor = Exception.class) public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqVO createReqVO) { // 获得流程定义 ProcessDefinition definition = processDefinitionService.getProcessDefinition(createReqVO.getProcessDefinitionId()); // 发起流程 return createProcessInstance0(userId, definition, createReqVO.getVariables(), null, createReqVO.getStartUserSelectAssignees()); } @Override public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO createReqDTO) { // 获得流程定义 ProcessDefinition definition = processDefinitionService.getActiveProcessDefinition(createReqDTO.getProcessDefinitionKey()); // 发起流程 return createProcessInstance0(userId, definition, createReqDTO.getVariables(), createReqDTO.getBusinessKey(), createReqDTO.getStartUserSelectAssignees()); } private String createProcessInstance0(Long userId, ProcessDefinition definition, Map<String, Object> variables, String businessKey, Map<String, List<Long>> startUserSelectAssignees) { // 1.1 校验流程定义 if (definition == null) { throw exception(PROCESS_DEFINITION_NOT_EXISTS); } if (definition.isSuspended()) { throw exception(PROCESS_DEFINITION_IS_SUSPENDED); } // 1.2 校验发起人自选审批人 validateStartUserSelectAssignees(definition, startUserSelectAssignees); // 2. 创建流程实例 if (variables == null) { variables = new HashMap<>(); } FlowableUtils.filterProcessInstanceFormVariable(variables); // 过滤一下,避免 ProcessInstance 系统级的变量被占用 variables.put(BpmConstants.PROCESS_INSTANCE_VARIABLE_STATUS, // 流程实例状态:审批中 BpmProcessInstanceStatusEnum.RUNNING.getStatus()); if (CollUtil.isNotEmpty(startUserSelectAssignees)) { variables.put(BpmConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, startUserSelectAssignees); } ProcessInstance instance = runtimeService.createProcessInstanceBuilder() .processDefinitionId(definition.getId()) .businessKey(businessKey) .name(definition.getName().trim()) .variables(variables) .start(); return instance.getId(); } private void validateStartUserSelectAssignees(ProcessDefinition definition, Map<String, List<Long>> startUserSelectAssignees) { // 1. 获得发起人自选审批人的 UserTask 列表 BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(definition.getId()); List<UserTask> userTaskList = BpmTaskCandidateStartUserSelectStrategy.getStartUserSelectUserTaskList(bpmnModel); if (CollUtil.isEmpty(userTaskList)) { return; } // 2. 校验发起人自选审批人的 UserTask 是否都配置了 userTaskList.forEach(userTask -> { List<Long> assignees = startUserSelectAssignees != null ? startUserSelectAssignees.get(userTask.getId()) : null; if (CollUtil.isEmpty(assignees)) { throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, userTask.getName()); } Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(assignees); assignees.forEach(assignee -> { if (userMap.get(assignee) == null) { throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_EXISTS, userTask.getName(), assignee); } }); }); } @Override public void cancelProcessInstanceByStartUser(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO) { // 1.1 校验流程实例存在 ProcessInstance instance = getProcessInstance(cancelReqVO.getId()); if (instance == null) { throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS); } // 1.2 只能取消自己的 if (!Objects.equals(instance.getStartUserId(), String.valueOf(userId))) { throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF); } // 2. 通过删除流程实例,实现流程实例的取消, // 删除流程实例,正则执行任务 ACT_RU_TASK. 任务会被删除。 deleteProcessInstance(cancelReqVO.getId(), BpmDeleteReasonEnum.CANCEL_PROCESS_INSTANCE_BY_START_USER.format(cancelReqVO.getReason())); // 3. 进一步的处理,交给 updateProcessInstanceCancel 方法 } @Override public void cancelProcessInstanceByAdmin(Long userId, BpmProcessInstanceCancelReqVO cancelReqVO) { // 1.1 校验流程实例存在 ProcessInstance instance = getProcessInstance(cancelReqVO.getId()); if (instance == null) { throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS); } // 1.2 管理员取消,不用校验是否为自己的 AdminUserRespDTO user = adminUserApi.getUser(userId); // 2. 通过删除流程实例,实现流程实例的取消, // 删除流程实例,正则执行任务 ACT_RU_TASK. 任务会被删除。 deleteProcessInstance(cancelReqVO.getId(), BpmDeleteReasonEnum.CANCEL_PROCESS_INSTANCE_BY_ADMIN.format(user.getNickname(), cancelReqVO.getReason())); // 3. 进一步的处理,交给 updateProcessInstanceCancel 方法 } @Override public void updateProcessInstanceWhenCancel(FlowableCancelledEvent event) { // 1. 判断是否为 Reject 不通过。如果是,则不进行更新. // 因为,updateProcessInstanceReject 方法(审批不通过),已经进行更新了 if (BpmDeleteReasonEnum.isRejectReason((String) event.getCause())) { return; } // 2. 更新流程实例 status runtimeService.setVariable(event.getProcessInstanceId(), BpmConstants.PROCESS_INSTANCE_VARIABLE_STATUS, BpmProcessInstanceStatusEnum.CANCEL.getStatus()); // 3. 发送流程实例的状态事件 // 注意:此时如果去查询 ProcessInstance 的话,字段是不全的,所以去查询了 HistoricProcessInstance HistoricProcessInstance processInstance = getHistoricProcessInstance(event.getProcessInstanceId()); // 发送流程实例的状态事件 processInstanceEventPublisher.sendProcessInstanceResultEvent( BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceStatusEvent(this, processInstance, BpmProcessInstanceStatusEnum.CANCEL.getStatus())); } @Override public void updateProcessInstanceWhenApprove(ProcessInstance instance) { // 1. 更新流程实例 status runtimeService.setVariable(instance.getId(), BpmConstants.PROCESS_INSTANCE_VARIABLE_STATUS, BpmProcessInstanceStatusEnum.APPROVE.getStatus()); // 2. 发送流程被【通过】的消息 messageService.sendMessageWhenProcessInstanceApprove(BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceApproveMessage(instance)); // 3. 发送流程实例的状态事件 // 注意:此时如果去查询 ProcessInstance 的话,字段是不全的,所以去查询了 HistoricProcessInstance HistoricProcessInstance processInstance = getHistoricProcessInstance(instance.getId()); processInstanceEventPublisher.sendProcessInstanceResultEvent( BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceStatusEvent(this, processInstance, BpmProcessInstanceStatusEnum.APPROVE.getStatus())); } @Override @Transactional(rollbackFor = Exception.class) public void updateProcessInstanceReject(String id, String reason) { // 1. 更新流程实例 status runtimeService.setVariable(id, BpmConstants.PROCESS_INSTANCE_VARIABLE_STATUS, BpmProcessInstanceStatusEnum.REJECT.getStatus()); // 2. 删除流程实例,以实现驳回任务时,取消整个审批流程 ProcessInstance processInstance = getProcessInstance(id); deleteProcessInstance(id, StrUtil.format(BpmDeleteReasonEnum.REJECT_TASK.format(reason))); // 3. 发送流程被【不通过】的消息 messageService.sendMessageWhenProcessInstanceReject(BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceRejectMessage(processInstance, reason)); // 4. 发送流程实例的状态事件 processInstanceEventPublisher.sendProcessInstanceResultEvent( BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceStatusEvent(this, processInstance, BpmProcessInstanceStatusEnum.REJECT.getStatus())); } private void deleteProcessInstance(String id, String reason) { runtimeService.deleteProcessInstance(id, reason); } }
3 3 \ No newline at end of file
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
... ... @@ -177,7 +177,7 @@ public class CrmBusinessController {
177 177 buildBusinessDetailList(list));
178 178 }
179 179  
180   - private List<CrmBusinessRespVO> buildBusinessDetailList(List<CrmBusinessDO> list) {
  180 + public List<CrmBusinessRespVO> buildBusinessDetailList(List<CrmBusinessDO> list) {
181 181 if (CollUtil.isEmpty(list)) {
182 182 return Collections.emptyList();
183 183 }
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsCustomerController.http
... ... @@ -53,3 +53,13 @@ tenant-id: {{adminTenentId}}
53 53 GET {{baseUrl}}/crm/statistics-customer/get-customer-deal-cycle-by-user?deptId=100&times[0]=2023-01-01 00:00:00&times[1]=2024-12-12 23:59:59
54 54 Authorization: Bearer {{token}}
55 55 tenant-id: {{adminTenentId}}
  56 +
  57 +### 6.3 获取客户成交周期(按区域)
  58 +GET {{baseUrl}}/crm/statistics-customer/get-customer-deal-cycle-by-area?deptId=100&times[0]=2023-01-01 00:00:00&times[1]=2024-12-12 23:59:59
  59 +Authorization: Bearer {{token}}
  60 +tenant-id: {{adminTenentId}}
  61 +
  62 +### 6.4 获取客户成交周期(按产品)
  63 +GET {{baseUrl}}/crm/statistics-customer/get-customer-deal-cycle-by-product?deptId=100&times[0]=2023-01-01 00:00:00&times[1]=2024-12-12 23:59:59
  64 +Authorization: Bearer {{token}}
  65 +tenant-id: {{adminTenentId}}
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsCustomerController.java
... ... @@ -96,6 +96,18 @@ public class CrmStatisticsCustomerController {
96 96 return success(customerService.getCustomerDealCycleByUser(reqVO));
97 97 }
98 98  
99   - // TODO dhb52:【成交周期分析】里,有按照员工(已实现)、地区(未实现)、产品(未实现),需要在看看哈;可以把 CustomerDealCycle 拆成 3 个 tab,员工客户成交周期分析、地区客户成交周期分析、产品客户成交周期分析;
  99 + @GetMapping("/get-customer-deal-cycle-by-area")
  100 + @Operation(summary = "获取客户成交周期(按用户)")
  101 + @PreAuthorize("@ss.hasPermission('crm:statistics-customer:query')")
  102 + public CommonResult<List<CrmStatisticsCustomerDealCycleByAreaRespVO>> getCustomerDealCycleByArea(@Valid CrmStatisticsCustomerReqVO reqVO) {
  103 + return success(customerService.getCustomerDealCycleByArea(reqVO));
  104 + }
  105 +
  106 + @GetMapping("/get-customer-deal-cycle-by-product")
  107 + @Operation(summary = "获取客户成交周期(按用户)")
  108 + @PreAuthorize("@ss.hasPermission('crm:statistics-customer:query')")
  109 + public CommonResult<List<CrmStatisticsCustomerDealCycleByProductRespVO>> getCustomerDealCycleByProduct(@Valid CrmStatisticsCustomerReqVO reqVO) {
  110 + return success(customerService.getCustomerDealCycleByProduct(reqVO));
  111 + }
100 112  
101 113 }
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsFunnelController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.crm.controller.admin.statistics;
  2 +
  3 +import cn.hutool.extra.spring.SpringUtil;
  4 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.module.crm.controller.admin.business.CrmBusinessController;
  7 +import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessRespVO;
  8 +import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.*;
  9 +import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
  10 +import cn.iocoder.yudao.module.crm.service.statistics.CrmStatisticsFunnelService;
  11 +import io.swagger.v3.oas.annotations.Operation;
  12 +import io.swagger.v3.oas.annotations.tags.Tag;
  13 +import jakarta.annotation.Resource;
  14 +import jakarta.validation.Valid;
  15 +import org.springframework.security.access.prepost.PreAuthorize;
  16 +import org.springframework.validation.annotation.Validated;
  17 +import org.springframework.web.bind.annotation.GetMapping;
  18 +import org.springframework.web.bind.annotation.RequestMapping;
  19 +import org.springframework.web.bind.annotation.RestController;
  20 +
  21 +import java.util.List;
  22 +
  23 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  24 +
  25 +@Tag(name = "管理后台 - CRM 销售漏斗")
  26 +@RestController
  27 +@RequestMapping("/crm/statistics-funnel")
  28 +@Validated
  29 +public class CrmStatisticsFunnelController {
  30 +
  31 + @Resource
  32 + private CrmStatisticsFunnelService funnelService;
  33 +
  34 + @GetMapping("/get-funnel-summary")
  35 + @Operation(summary = "获取销售漏斗统计数据", description = "用于【销售漏斗】页面的【销售漏斗分析】")
  36 + @PreAuthorize("@ss.hasPermission('crm:statistics-funnel:query')")
  37 + public CommonResult<CrmStatisticFunnelSummaryRespVO> getFunnelSummary(@Valid CrmStatisticsFunnelReqVO reqVO) {
  38 + return success(funnelService.getFunnelSummary(reqVO));
  39 + }
  40 +
  41 + @GetMapping("/get-business-summary-by-end-status")
  42 + @Operation(summary = "获取商机结束状态统计", description = "用于【销售漏斗】页面的【销售漏斗分析】")
  43 + @PreAuthorize("@ss.hasPermission('crm:statistics-funnel:query')")
  44 + public CommonResult<List<CrmStatisticsBusinessSummaryByEndStatusRespVO>> getBusinessSummaryByEndStatus(@Valid CrmStatisticsFunnelReqVO reqVO) {
  45 + return success(funnelService.getBusinessSummaryByEndStatus(reqVO));
  46 + }
  47 +
  48 + @GetMapping("/get-business-summary-by-date")
  49 + @Operation(summary = "获取新增商机分析(按日期)", description = "用于【销售漏斗】页面")
  50 + @PreAuthorize("@ss.hasPermission('crm:statistics-funnel:query')")
  51 + public CommonResult<List<CrmStatisticsBusinessSummaryByDateRespVO>> getBusinessSummaryByDate(@Valid CrmStatisticsFunnelReqVO reqVO) {
  52 + return success(funnelService.getBusinessSummaryByDate(reqVO));
  53 + }
  54 +
  55 + @GetMapping("/get-business-inversion-rate-summary-by-date")
  56 + @Operation(summary = "获取商机转化率分析(按日期)", description = "用于【销售漏斗】页面")
  57 + @PreAuthorize("@ss.hasPermission('crm:statistics-funnel:query')")
  58 + public CommonResult<List<CrmStatisticsBusinessInversionRateSummaryByDateRespVO>> getBusinessInversionRateSummaryByDate(@Valid CrmStatisticsFunnelReqVO reqVO) {
  59 + return success(funnelService.getBusinessInversionRateSummaryByDate(reqVO));
  60 + }
  61 +
  62 + @GetMapping("/get-business-page-by-date")
  63 + @Operation(summary = "获得商机分页(按日期)", description = "用于【销售漏斗】页面的【新增商机分析】")
  64 + @PreAuthorize("@ss.hasPermission('crm:business:query')")
  65 + public CommonResult<PageResult<CrmBusinessRespVO>> getBusinessPageByDate(@Valid CrmStatisticsFunnelReqVO pageVO) {
  66 + PageResult<CrmBusinessDO> pageResult = funnelService.getBusinessPageByDate(pageVO);
  67 + return success(new PageResult<>(buildBusinessDetailList(pageResult.getList()), pageResult.getTotal()));
  68 + }
  69 +
  70 + private List<CrmBusinessRespVO> buildBusinessDetailList(List<CrmBusinessDO> list) {
  71 + return SpringUtil.getBean(CrmBusinessController.class).buildBusinessDetailList(list);
  72 + }
  73 +
  74 +}
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerDealCycleByAreaRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonIgnore;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +
  7 +@Schema(description = "管理后台 - CRM 客户成交周期分析(按区域) VO")
  8 +@Data
  9 +public class CrmStatisticsCustomerDealCycleByAreaRespVO {
  10 +
  11 + @Schema(description = "省份编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  12 + @JsonIgnore
  13 + private Integer areaId;
  14 +
  15 + @Schema(description = "省份名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "浙江省")
  16 + private String areaName;
  17 +
  18 + @Schema(description = "成交周期", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0")
  19 + private Double customerDealCycle;
  20 +
  21 + @Schema(description = "成交客户数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  22 + private Integer customerDealCount;
  23 +
  24 +}
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerDealCycleByProductRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +@Schema(description = "管理后台 - CRM 客户成交周期分析(按产品) VO")
  7 +@Data
  8 +public class CrmStatisticsCustomerDealCycleByProductRespVO {
  9 +
  10 + @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "演示产品")
  11 + private String productName;
  12 +
  13 + @Schema(description = "成交周期", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0")
  14 + private Double customerDealCycle;
  15 +
  16 + @Schema(description = "成交客户数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  17 + private Integer customerDealCount;
  18 +
  19 +}
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticFunnelSummaryRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Data;
  6 +import lombok.NoArgsConstructor;
  7 +
  8 +@Schema(description = "管理后台 - CRM 销售漏斗 Response VO")
  9 +@NoArgsConstructor
  10 +@AllArgsConstructor
  11 +@Data
  12 +public class CrmStatisticFunnelSummaryRespVO {
  13 +
  14 + @Schema(description = "客户数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  15 + private Long customerCount;
  16 +
  17 + @Schema(description = "商机数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  18 + private Long businessCount;
  19 +
  20 + @Schema(description = "赢单数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  21 + private Long businessWinCount;
  22 +
  23 +}
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsBusinessInversionRateSummaryByDateRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.math.BigDecimal;
  7 +
  8 +@Schema(description = "管理后台 - CRM 商机转化率分析(按日期) VO")
  9 +@Data
  10 +public class CrmStatisticsBusinessInversionRateSummaryByDateRespVO {
  11 +
  12 + @Schema(description = "时间轴", requiredMode = Schema.RequiredMode.REQUIRED, example = "202401")
  13 + private String time;
  14 +
  15 + @Schema(description = "商机数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  16 + private Long businessCount;
  17 +
  18 + @Schema(description = "赢单商机数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  19 + private Long businessWinCount;
  20 +
  21 +}
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsBusinessSummaryByDateRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.math.BigDecimal;
  7 +
  8 +@Schema(description = "管理后台 - CRM 新增商机分析(按日期) VO")
  9 +@Data
  10 +public class CrmStatisticsBusinessSummaryByDateRespVO {
  11 +
  12 + @Schema(description = "时间轴", requiredMode = Schema.RequiredMode.REQUIRED, example = "202401")
  13 + private String time;
  14 +
  15 + @Schema(description = "新增商机数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  16 + private Long businessCreateCount;
  17 +
  18 + @Schema(description = "新增商机金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  19 + private BigDecimal totalPrice;
  20 +
  21 +}
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsBusinessSummaryByEndStatusRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Data;
  6 +import lombok.NoArgsConstructor;
  7 +
  8 +import java.math.BigDecimal;
  9 +
  10 +@Schema(description = "管理后台 - CRM 商机结束状态统计 Response VO")
  11 +@NoArgsConstructor
  12 +@AllArgsConstructor
  13 +@Data
  14 +public class CrmStatisticsBusinessSummaryByEndStatusRespVO {
  15 +
  16 + @Schema(description = "结束状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  17 + private Integer endStatus;
  18 +
  19 + @Schema(description = "商机数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  20 + private Long businessCount;
  21 +
  22 + @Schema(description = "商机总金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  23 + private BigDecimal totalPrice;
  24 +
  25 +}
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsFunnelReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.DateIntervalEnum;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  5 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  6 +import io.swagger.v3.oas.annotations.media.Schema;
  7 +import jakarta.validation.constraints.NotNull;
  8 +import jakarta.validation.constraints.Size;
  9 +import lombok.Data;
  10 +import org.springframework.format.annotation.DateTimeFormat;
  11 +
  12 +import java.time.LocalDateTime;
  13 +import java.util.List;
  14 +
  15 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  16 +
  17 +@Schema(description = "管理后台 - CRM 销售漏斗 Request VO")
  18 +@Data
  19 +public class CrmStatisticsFunnelReqVO extends PageParam {
  20 +
  21 + @Schema(description = "部门 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  22 + @NotNull(message = "部门 id 不能为空")
  23 + private Long deptId;
  24 +
  25 + /**
  26 + * 负责人用户 id, 当用户为空, 则计算部门下用户
  27 + */
  28 + @Schema(description = "负责人用户 id", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "1")
  29 + private Long userId;
  30 +
  31 + /**
  32 + * userIds 目前不用前端传递,目前是方便后端通过 deptId 读取编号后,设置回来
  33 + * 后续,可能会支持选择部分用户进行查询
  34 + */
  35 + @Schema(description = "负责人用户 id 集合", hidden = true, example = "2")
  36 + private List<Long> userIds;
  37 +
  38 + @Schema(description = "时间间隔类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  39 + @InEnum(value = DateIntervalEnum.class, message = "时间间隔类型,必须是 {value}")
  40 + private Integer interval;
  41 +
  42 + @Schema(description = "时间范围", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
  43 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  44 + @Size(min = 2, max = 2, message = "请选择时间范围")
  45 + private LocalDateTime[] times;
  46 +
  47 +}
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/portrait/CrmStatisticsPortraitReqVO.java
... ... @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.portrait;
2 2  
3 3 import io.swagger.v3.oas.annotations.media.Schema;
4 4 import jakarta.validation.constraints.NotNull;
  5 +import jakarta.validation.constraints.Size;
5 6 import lombok.Data;
6 7 import org.springframework.format.annotation.DateTimeFormat;
7 8  
... ... @@ -31,12 +32,9 @@ public class CrmStatisticsPortraitReqVO {
31 32 @Schema(description = "负责人用户 id 集合", hidden = true, example = "2")
32 33 private List<Long> userIds;
33 34  
34   - /**
35   - * 前端如果选择自定义时间, 那么前端传递起始-终止时间, 如果选择其他时间间隔类型, 则由后台计算起始-终止时间
36   - * 并作为参数传递给Mapper
37   - */
38 35 @Schema(description = "时间范围", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
39 36 @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  37 + @Size(min = 2, max = 2, message = "请选择时间范围")
40 38 private LocalDateTime[] times;
41 39  
42 40 }
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
... ... @@ -5,8 +5,8 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6 6 import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
7 7 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessPageReqVO;
  8 +import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsFunnelReqVO;
8 9 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
9   -import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
10 10 import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
11 11 import cn.iocoder.yudao.module.crm.util.CrmPermissionUtils;
12 12 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
... ... @@ -59,10 +59,16 @@ public interface CrmBusinessMapper extends BaseMapperX&lt;CrmBusinessDO&gt; {
59 59 return selectCount(CrmBusinessDO::getStatusTypeId, statusTypeId);
60 60 }
61 61  
62   - default List<CrmBusinessDO> selectListByCustomerIdOwnerUserId(Long customerId, Long ownerUserId){
  62 + default List<CrmBusinessDO> selectListByCustomerIdOwnerUserId(Long customerId, Long ownerUserId) {
63 63 return selectList(new LambdaQueryWrapperX<CrmBusinessDO>()
64 64 .eq(CrmBusinessDO::getCustomerId, customerId)
65 65 .eq(CrmBusinessDO::getOwnerUserId, ownerUserId));
66 66 }
67 67  
  68 + default PageResult<CrmBusinessDO> selectPage(CrmStatisticsFunnelReqVO pageVO) {
  69 + return selectPage(pageVO, new LambdaQueryWrapperX<CrmBusinessDO>()
  70 + .in(CrmBusinessDO::getOwnerUserId, pageVO.getUserIds())
  71 + .betweenIfPresent(CrmBusinessDO::getCreateTime, pageVO.getTimes()));
  72 + }
  73 +
68 74 }
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/statistics/CrmStatisticsCustomerMapper.java
... ... @@ -53,6 +53,7 @@ public interface CrmStatisticsCustomerMapper {
53 53  
54 54 /**
55 55 * 合同总金额(按用户)
  56 + *
56 57 * @return 统计数据@return 统计数据@param reqVO 请求参数
57 58 * @return 统计数据
58 59 */
... ... @@ -191,4 +192,20 @@ public interface CrmStatisticsCustomerMapper {
191 192 */
192 193 List<CrmStatisticsCustomerDealCycleByUserRespVO> selectCustomerDealCycleGroupByUser(CrmStatisticsCustomerReqVO reqVO);
193 194  
  195 + /**
  196 + * 客户成交周期(按区域)
  197 + *
  198 + * @param reqVO 请求参数
  199 + * @return 统计数据
  200 + */
  201 + List<CrmStatisticsCustomerDealCycleByAreaRespVO> selectCustomerDealCycleGroupByAreaId(CrmStatisticsCustomerReqVO reqVO);
  202 +
  203 + /**
  204 + * 客户成交周期(按产品)
  205 + *
  206 + * @param reqVO 请求参数
  207 + * @return 统计数据
  208 + */
  209 + List<CrmStatisticsCustomerDealCycleByProductRespVO> selectCustomerDealCycleGroupByProductId(CrmStatisticsCustomerReqVO reqVO);
  210 +
194 211 }
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/statistics/CrmStatisticsFunnelMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.crm.dal.mysql.statistics;
  2 +
  3 +import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsBusinessInversionRateSummaryByDateRespVO;
  4 +import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsBusinessSummaryByDateRespVO;
  5 +import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsBusinessSummaryByEndStatusRespVO;
  6 +import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsFunnelReqVO;
  7 +import org.apache.ibatis.annotations.Mapper;
  8 +import org.apache.ibatis.annotations.Param;
  9 +
  10 +import java.util.List;
  11 +
  12 +/**
  13 + * CRM 销售漏斗 Mapper
  14 + *
  15 + * @author HUIHUI
  16 + */
  17 +@Mapper
  18 +public interface CrmStatisticsFunnelMapper {
  19 +
  20 + Long selectCustomerCountByDate(CrmStatisticsFunnelReqVO reqVO);
  21 +
  22 + Long selectBusinessCountByDateAndEndStatus(@Param("reqVO") CrmStatisticsFunnelReqVO reqVO, @Param("status") Integer status);
  23 +
  24 + List<CrmStatisticsBusinessSummaryByEndStatusRespVO> selectBusinessSummaryListGroupByEndStatus(CrmStatisticsFunnelReqVO reqVO);
  25 +
  26 + List<CrmStatisticsBusinessSummaryByDateRespVO> selectBusinessSummaryGroupByDate(CrmStatisticsFunnelReqVO reqVO);
  27 +
  28 + List<CrmStatisticsBusinessInversionRateSummaryByDateRespVO> selectBusinessInversionRateSummaryByDate(CrmStatisticsFunnelReqVO reqVO);
  29 +
  30 +}
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
... ... @@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusi
5 5 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessSaveReqVO;
6 6 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessTransferReqVO;
7 7 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessUpdateStatusReqVO;
  8 +import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsFunnelReqVO;
8 9 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
9 10 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessProductDO;
10 11 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessStatusDO;
... ... @@ -194,4 +195,12 @@ public interface CrmBusinessService {
194 195 */
195 196 List<CrmBusinessDO> getBusinessListByCustomerIdOwnerUserId(Long customerId, Long ownerUserId);
196 197  
  198 + /**
  199 + * 获得商机分页,目前用于【数据统计】
  200 + *
  201 + * @param pageVO 请求
  202 + * @return 商机分页
  203 + */
  204 + PageResult<CrmBusinessDO> getBusinessPageByDate(CrmStatisticsFunnelReqVO pageVO);
  205 +
197 206 }
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
... ... @@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusi
10 10 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessTransferReqVO;
11 11 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessUpdateStatusReqVO;
12 12 import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.CrmContactBusinessReqVO;
  13 +import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsFunnelReqVO;
13 14 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
14 15 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessProductDO;
15 16 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessStatusDO;
... ... @@ -375,4 +376,9 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
375 376 return businessMapper.selectListByCustomerIdOwnerUserId(customerId, ownerUserId);
376 377 }
377 378  
  379 + @Override
  380 + public PageResult<CrmBusinessDO> getBusinessPageByDate(CrmStatisticsFunnelReqVO pageVO) {
  381 + return businessMapper.selectPage(pageVO);
  382 + }
  383 +
378 384 }
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java
... ... @@ -120,7 +120,7 @@ public class CrmClueServiceImpl implements CrmClueService {
120 120 }
121 121  
122 122 @Override
123   - @LogRecord(type = CRM_CLUE_TYPE, subType = CRM_CLUE_FOLLOW_UP_SUB_TYPE, bizNo = "{{#id}",
  123 + @LogRecord(type = CRM_CLUE_TYPE, subType = CRM_CLUE_FOLLOW_UP_SUB_TYPE, bizNo = "{{#id}}",
124 124 success = CRM_CLUE_FOLLOW_UP_SUCCESS)
125 125 @CrmPermission(bizType = CrmBizTypeEnum.CRM_CLUE, bizId = "#id", level = CrmPermissionLevelEnum.WRITE)
126 126 public void updateClueFollowUp(Long id, LocalDateTime contactNextTime, String contactLastContent) {
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
... ... @@ -39,7 +39,7 @@ public interface CrmCustomerService {
39 39 /**
40 40 * 更新客户的跟进状态
41 41 *
42   - * @param id 编号
  42 + * @param id 编号
43 43 * @param dealStatus 跟进状态
44 44 */
45 45 void updateCustomerDealStatus(Long id, Boolean dealStatus);
... ... @@ -47,8 +47,8 @@ public interface CrmCustomerService {
47 47 /**
48 48 * 更新客户相关的跟进信息
49 49 *
50   - * @param id 编号
51   - * @param contactNextTime 下次联系时间
  50 + * @param id 编号
  51 + * @param contactNextTime 下次联系时间
52 52 * @param contactLastContent 最后联系内容
53 53 */
54 54 void updateCustomerFollowUp(Long id, LocalDateTime contactNextTime, String contactLastContent);
... ... @@ -99,8 +99,8 @@ public interface CrmCustomerService {
99 99 /**
100 100 * 获得放入公海提醒的客户分页
101 101 *
102   - * @param pageVO 分页查询
103   - * @param userId 用户编号
  102 + * @param pageVO 分页查询
  103 + * @param userId 用户编号
104 104 * @return 客户分页
105 105 */
106 106 PageResult<CrmCustomerDO> getPutPoolRemindCustomerPage(CrmCustomerPageReqVO pageVO, Long userId);
... ... @@ -108,7 +108,7 @@ public interface CrmCustomerService {
108 108 /**
109 109 * 获得待进入公海的客户数量
110 110 *
111   - * @param userId 用户编号
  111 + * @param userId 用户编号
112 112 * @return 提醒数量
113 113 */
114 114 Long getPutPoolRemindCustomerCount(Long userId);
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
... ... @@ -47,6 +47,7 @@ import java.time.LocalDateTime;
47 47 import java.util.*;
48 48  
49 49 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  50 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
50 51 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
51 52 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
52 53 import static cn.iocoder.yudao.module.crm.enums.LogRecordConstants.*;
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsCustomerService.java
... ... @@ -93,4 +93,20 @@ public interface CrmStatisticsCustomerService {
93 93 */
94 94 List<CrmStatisticsCustomerDealCycleByUserRespVO> getCustomerDealCycleByUser(CrmStatisticsCustomerReqVO reqVO);
95 95  
  96 + /**
  97 + * 客户成交周期(按区域)
  98 + *
  99 + * @param reqVO 请求参数
  100 + * @return 统计数据
  101 + */
  102 + List<CrmStatisticsCustomerDealCycleByAreaRespVO> getCustomerDealCycleByArea(CrmStatisticsCustomerReqVO reqVO);
  103 +
  104 + /**
  105 + * 客户成交周期(按产品)
  106 + *
  107 + * @param reqVO 请求参数
  108 + * @return 统计数据
  109 + */
  110 + List<CrmStatisticsCustomerDealCycleByProductRespVO> getCustomerDealCycleByProduct(CrmStatisticsCustomerReqVO reqVO);
  111 +
96 112 }
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsCustomerServiceImpl.java
... ... @@ -4,6 +4,9 @@ import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.util.ObjUtil;
5 5 import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
6 6 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
  7 +import cn.iocoder.yudao.framework.ip.core.Area;
  8 +import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
  9 +import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
7 10 import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.*;
8 11 import cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsCustomerMapper;
9 12 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
... ... @@ -19,6 +22,7 @@ import java.time.LocalDateTime;
19 22 import java.util.Collections;
20 23 import java.util.List;
21 24 import java.util.Map;
  25 +import java.util.function.Function;
22 26 import java.util.stream.Stream;
23 27  
24 28 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
... ... @@ -290,6 +294,46 @@ public class CrmStatisticsCustomerServiceImpl implements CrmStatisticsCustomerSe
290 294 return summaryList;
291 295 }
292 296  
  297 + @Override
  298 + public List<CrmStatisticsCustomerDealCycleByAreaRespVO> getCustomerDealCycleByArea(CrmStatisticsCustomerReqVO reqVO) {
  299 + // 1. 获得用户编号数组
  300 + List<Long> userIds = getUserIds(reqVO);
  301 + if (CollUtil.isEmpty(userIds)) {
  302 + return Collections.emptyList();
  303 + }
  304 + reqVO.setUserIds(userIds);
  305 +
  306 + // 2. 获取客户地区统计数据
  307 + List<CrmStatisticsCustomerDealCycleByAreaRespVO> dealCycleByAreaList = customerMapper.selectCustomerDealCycleGroupByAreaId(reqVO);
  308 + if (CollUtil.isEmpty(dealCycleByAreaList)) {
  309 + return Collections.emptyList();
  310 + }
  311 +
  312 + // 3. 拼接数据
  313 + Map<Integer, Area> areaMap = convertMap(AreaUtils.getByType(AreaTypeEnum.PROVINCE, Function.identity()), Area::getId);
  314 + return convertList(dealCycleByAreaList, vo -> {
  315 + if (vo.getAreaId() != null) {
  316 + Integer parentId = AreaUtils.getParentIdByType(vo.getAreaId(), AreaTypeEnum.PROVINCE);
  317 + findAndThen(areaMap, parentId, area -> vo.setAreaId(parentId).setAreaName(area.getName()));
  318 + }
  319 + return vo;
  320 + });
  321 + }
  322 +
  323 + @Override
  324 + public List<CrmStatisticsCustomerDealCycleByProductRespVO> getCustomerDealCycleByProduct(CrmStatisticsCustomerReqVO reqVO) {
  325 + // 1. 获得用户编号数组
  326 + List<Long> userIds = getUserIds(reqVO);
  327 + if (CollUtil.isEmpty(userIds)) {
  328 + return Collections.emptyList();
  329 + }
  330 + reqVO.setUserIds(userIds);
  331 +
  332 + // 2. 获取客户产品统计数据
  333 + // TODO @dhb52:未读取产品名
  334 + return customerMapper.selectCustomerDealCycleGroupByProductId(reqVO);
  335 + }
  336 +
293 337 /**
294 338 * 拼接用户信息(昵称)
295 339 *
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsFunnelService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.crm.service.statistics;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.*;
  5 +import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
  6 +
  7 +import java.util.List;
  8 +
  9 +/**
  10 + * CRM 销售漏斗分析 Service
  11 + *
  12 + * @author HUIHUI
  13 + */
  14 +public interface CrmStatisticsFunnelService {
  15 +
  16 + /**
  17 + * 获得销售漏斗数据
  18 + *
  19 + * @param reqVO 请求
  20 + * @return 销售漏斗数据
  21 + */
  22 + CrmStatisticFunnelSummaryRespVO getFunnelSummary(CrmStatisticsFunnelReqVO reqVO);
  23 +
  24 + /**
  25 + * 获得商机结束状态统计
  26 + *
  27 + * @param reqVO 请求
  28 + * @return 商机结束状态统计
  29 + */
  30 + List<CrmStatisticsBusinessSummaryByEndStatusRespVO> getBusinessSummaryByEndStatus(CrmStatisticsFunnelReqVO reqVO);
  31 +
  32 + /**
  33 + * 获取新增商机分析(按日期)
  34 + *
  35 + * @param reqVO 请求
  36 + * @return 新增商机分析
  37 + */
  38 + List<CrmStatisticsBusinessSummaryByDateRespVO> getBusinessSummaryByDate(CrmStatisticsFunnelReqVO reqVO);
  39 +
  40 + /**
  41 + * 获得商机转化率分析(按日期)
  42 + *
  43 + * @param reqVO 请求
  44 + * @return 商机转化率分析
  45 + */
  46 + List<CrmStatisticsBusinessInversionRateSummaryByDateRespVO> getBusinessInversionRateSummaryByDate(CrmStatisticsFunnelReqVO reqVO);
  47 +
  48 + /**
  49 + * 获得商机分页(按日期)
  50 + *
  51 + * @param pageVO 请求
  52 + * @return 商机分页
  53 + */
  54 + PageResult<CrmBusinessDO> getBusinessPageByDate(CrmStatisticsFunnelReqVO pageVO);
  55 +
  56 +}
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsFunnelServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.crm.service.statistics;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.ObjUtil;
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
  7 +import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.*;
  8 +import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
  9 +import cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsFunnelMapper;
  10 +import cn.iocoder.yudao.module.crm.enums.business.CrmBusinessEndStatusEnum;
  11 +import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
  12 +import cn.iocoder.yudao.module.system.api.dept.DeptApi;
  13 +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
  14 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  15 +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
  16 +import jakarta.annotation.Resource;
  17 +import org.springframework.stereotype.Service;
  18 +
  19 +import java.math.BigDecimal;
  20 +import java.time.LocalDateTime;
  21 +import java.util.Collections;
  22 +import java.util.List;
  23 +
  24 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  25 +
  26 +/**
  27 + * CRM 销售漏斗分析 Service 实现类
  28 + *
  29 + * @author HUIHUI
  30 + */
  31 +@Service
  32 +public class CrmStatisticsFunnelServiceImpl implements CrmStatisticsFunnelService {
  33 +
  34 + @Resource
  35 + private CrmStatisticsFunnelMapper funnelMapper;
  36 +
  37 + @Resource
  38 + private AdminUserApi adminUserApi;
  39 + @Resource
  40 + private CrmBusinessService businessService;
  41 + @Resource
  42 + private DeptApi deptApi;
  43 +
  44 + @Override
  45 + public CrmStatisticFunnelSummaryRespVO getFunnelSummary(CrmStatisticsFunnelReqVO reqVO) {
  46 + // 1. 获得用户编号数组
  47 + List<Long> userIds = getUserIds(reqVO);
  48 + if (CollUtil.isEmpty(userIds)) {
  49 + return null;
  50 + }
  51 + reqVO.setUserIds(userIds);
  52 +
  53 + // 2. 获得漏斗数据
  54 + Long customerCount = funnelMapper.selectCustomerCountByDate(reqVO);
  55 + Long businessCount = funnelMapper.selectBusinessCountByDateAndEndStatus(reqVO, null);
  56 + Long businessWinCount = funnelMapper.selectBusinessCountByDateAndEndStatus(reqVO, CrmBusinessEndStatusEnum.WIN.getStatus());
  57 + return new CrmStatisticFunnelSummaryRespVO(customerCount, businessCount, businessWinCount);
  58 + }
  59 +
  60 + @Override
  61 + public List<CrmStatisticsBusinessSummaryByEndStatusRespVO> getBusinessSummaryByEndStatus(CrmStatisticsFunnelReqVO reqVO) {
  62 + // 1. 获得用户编号数组
  63 + reqVO.setUserIds(getUserIds(reqVO));
  64 + if (CollUtil.isEmpty(reqVO.getUserIds())) {
  65 + return Collections.emptyList();
  66 + }
  67 +
  68 + // 2. 获得统计数据
  69 + return funnelMapper.selectBusinessSummaryListGroupByEndStatus(reqVO);
  70 + }
  71 +
  72 + @Override
  73 + public List<CrmStatisticsBusinessSummaryByDateRespVO> getBusinessSummaryByDate(CrmStatisticsFunnelReqVO reqVO) {
  74 + // 1. 获得用户编号数组
  75 + reqVO.setUserIds(getUserIds(reqVO));
  76 + if (CollUtil.isEmpty(reqVO.getUserIds())) {
  77 + return Collections.emptyList();
  78 + }
  79 +
  80 + // 2. 按天统计,获取分项统计数据
  81 + List<CrmStatisticsBusinessSummaryByDateRespVO> businessSummaryList = funnelMapper.selectBusinessSummaryGroupByDate(reqVO);
  82 + // 3. 按照日期间隔,合并数据
  83 + List<LocalDateTime[]> timeRanges = LocalDateTimeUtils.getDateRangeList(reqVO.getTimes()[0], reqVO.getTimes()[1], reqVO.getInterval());
  84 + return convertList(timeRanges, times -> {
  85 + Long businessCreateCount = businessSummaryList.stream()
  86 + .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], vo.getTime()))
  87 + .mapToLong(CrmStatisticsBusinessSummaryByDateRespVO::getBusinessCreateCount).sum();
  88 + BigDecimal businessDealCount = businessSummaryList.stream()
  89 + .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], vo.getTime()))
  90 + .map(CrmStatisticsBusinessSummaryByDateRespVO::getTotalPrice)
  91 + .reduce(BigDecimal.ZERO, BigDecimal::add);
  92 + return new CrmStatisticsBusinessSummaryByDateRespVO()
  93 + .setTime(LocalDateTimeUtils.formatDateRange(times[0], times[1], reqVO.getInterval()))
  94 + .setBusinessCreateCount(businessCreateCount).setTotalPrice(businessDealCount);
  95 + });
  96 + }
  97 +
  98 + @Override
  99 + public List<CrmStatisticsBusinessInversionRateSummaryByDateRespVO> getBusinessInversionRateSummaryByDate(CrmStatisticsFunnelReqVO reqVO) {
  100 + // 1. 获得用户编号数组
  101 + reqVO.setUserIds(getUserIds(reqVO));
  102 + if (CollUtil.isEmpty(reqVO.getUserIds())) {
  103 + return Collections.emptyList();
  104 + }
  105 +
  106 + // 2. 按天统计,获取分项统计数据
  107 + List<CrmStatisticsBusinessInversionRateSummaryByDateRespVO> businessSummaryList = funnelMapper.selectBusinessInversionRateSummaryByDate(reqVO);
  108 + // 3. 按照日期间隔,合并数据
  109 + List<LocalDateTime[]> timeRanges = LocalDateTimeUtils.getDateRangeList(reqVO.getTimes()[0], reqVO.getTimes()[1], reqVO.getInterval());
  110 + return convertList(timeRanges, times -> {
  111 + Long businessCount = businessSummaryList.stream()
  112 + .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], vo.getTime()))
  113 + .mapToLong(CrmStatisticsBusinessInversionRateSummaryByDateRespVO::getBusinessCount).sum();
  114 + Long businessWinCount = businessSummaryList.stream()
  115 + .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], vo.getTime()))
  116 + .mapToLong(CrmStatisticsBusinessInversionRateSummaryByDateRespVO::getBusinessWinCount).sum();
  117 + return new CrmStatisticsBusinessInversionRateSummaryByDateRespVO()
  118 + .setTime(LocalDateTimeUtils.formatDateRange(times[0], times[1], reqVO.getInterval()))
  119 + .setBusinessCount(businessCount).setBusinessWinCount(businessWinCount);
  120 + });
  121 + }
  122 +
  123 + @Override
  124 + public PageResult<CrmBusinessDO> getBusinessPageByDate(CrmStatisticsFunnelReqVO pageVO) {
  125 + // 1. 获得用户编号数组
  126 + pageVO.setUserIds(getUserIds(pageVO));
  127 + if (CollUtil.isEmpty(pageVO.getUserIds())) {
  128 + return PageResult.empty();
  129 + }
  130 + // 2. 执行查询
  131 + return businessService.getBusinessPageByDate(pageVO);
  132 + }
  133 +
  134 + /**
  135 + * 获取用户编号数组。如果用户编号为空, 则获得部门下的用户编号数组,包括子部门的所有用户编号
  136 + *
  137 + * @param reqVO 请求参数
  138 + * @return 用户编号数组
  139 + */
  140 + private List<Long> getUserIds(CrmStatisticsFunnelReqVO reqVO) {
  141 + // 情况一:选中某个用户
  142 + if (ObjUtil.isNotNull(reqVO.getUserId())) {
  143 + return List.of(reqVO.getUserId());
  144 + }
  145 + // 情况二:选中某个部门
  146 + // 2.1 获得部门列表
  147 + List<Long> deptIds = convertList(deptApi.getChildDeptList(reqVO.getDeptId()), DeptRespDTO::getId);
  148 + deptIds.add(reqVO.getDeptId());
  149 + // 2.2 获得用户编号
  150 + return convertList(adminUserApi.getUserListByDeptIds(deptIds), AdminUserRespDTO::getId);
  151 + }
  152 +
  153 +}
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPerformanceServiceImpl.java
1 1 package cn.iocoder.yudao.module.crm.service.statistics;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.date.LocalDateTimeUtil;
4 5 import cn.hutool.core.util.ObjUtil;
5 6 import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceReqVO;
6 7 import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO;
... ... @@ -13,8 +14,9 @@ import org.springframework.stereotype.Service;
13 14 import org.springframework.validation.annotation.Validated;
14 15  
15 16 import jakarta.annotation.Resource;
16   -import java.util.Collections;
17   -import java.util.List;
  17 +
  18 +import java.math.BigDecimal;
  19 +import java.util.*;
18 20 import java.util.function.Function;
19 21  
20 22 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
... ... @@ -38,7 +40,7 @@ public class CrmStatisticsPerformanceServiceImpl implements CrmStatisticsPerform
38 40  
39 41 @Override
40 42 public List<CrmStatisticsPerformanceRespVO> getContractCountPerformance(CrmStatisticsPerformanceReqVO performanceReqVO) {
41   - // TODO @scholar:我们可以换个思路实现,减少数据库的计算量;
  43 + // TODO @scholar:可以把下面这个注释,你理解后,重新整理下,写到 getPerformance 里;
42 44 // 比如说,2024 年的合同数据,是不是 2022-12 到 2024-12-31,每个月的统计呢?
43 45 // 理解之后,我们可以数据 group by 年-月,20222-12 到 2024-12-31 的,然后内存在聚合出 CrmStatisticsPerformanceRespVO 这样
44 46 // 这样,我们就可以减少数据库的计算量,提升性能;同时 SQL 也会很简单,开发者理解起来也简单哈;
... ... @@ -55,28 +57,99 @@ public class CrmStatisticsPerformanceServiceImpl implements CrmStatisticsPerform
55 57 return getPerformance(performanceReqVO, performanceMapper::selectReceivablePricePerformance);
56 58 }
57 59  
  60 + // TODO @scholar:代码注释,应该有 3 个变量哈;
58 61 /**
59 62 * 获得员工业绩数据
60 63 *
61 64 * @param performanceReqVO 参数
62   - * @param performanceFunction 排行榜方法
63   - * @return 排行版数据
  65 + * @param performanceFunction 员工业绩统计方法
  66 + * @return 员工业绩数据
64 67 */
  68 + // TODO @scholar:下面一行的变量,超过一行了,阅读不美观;可以考虑每一行一个变量;
65 69 private List<CrmStatisticsPerformanceRespVO> getPerformance(CrmStatisticsPerformanceReqVO performanceReqVO, Function<CrmStatisticsPerformanceReqVO,
66 70 List<CrmStatisticsPerformanceRespVO>> performanceFunction) {
67 71  
  72 + // TODO @scholar:没使用到的变量,建议删除;
  73 + List<CrmStatisticsPerformanceRespVO> performanceRespVOList;
  74 +
68 75 // 1. 获得用户编号数组
69 76 final List<Long> userIds = getUserIds(performanceReqVO);
70 77 if (CollUtil.isEmpty(userIds)) {
71 78 return Collections.emptyList();
72 79 }
73 80 performanceReqVO.setUserIds(userIds);
74   - // 2. 获得排行数据
  81 + // TODO @scholar:1. 和 2. 之间,可以考虑换一行;保证每一块逻辑的间隔;
  82 + // 2. 获得业绩数据
  83 + // TODO @scholar:复数变量,建议使用 s 或者 list 结果;这里用 performanceList 好列;
75 84 List<CrmStatisticsPerformanceRespVO> performance = performanceFunction.apply(performanceReqVO);
76   - if (CollUtil.isEmpty(performance)) {
77   - return Collections.emptyList();
  85 +
  86 + // 获取查询的年份
  87 + // TODO @scholar:逻辑可以简化一下;
  88 + // TODO 1)把 performance 转换成 map;key 是 time,value 是 count
  89 + // TODO 2)当前年,遍历 1-12 月份,去 map 拿到 count;接着月份 -1,去 map 拿 count;再年份 -1,拿 count
  90 + String currentYear = LocalDateTimeUtil.format(performanceReqVO.getTimes()[0],"yyyy");
  91 +
  92 + // 构造查询当年和前一年,每年12个月的年月组合
  93 + List<String> allMonths = new ArrayList<>();
  94 + for (int year = Integer.parseInt(currentYear)-1; year <= Integer.parseInt(currentYear); year++) {
  95 + for (int month = 1; month <= 12; month++) {
  96 + allMonths.add(String.format("%d%02d", year, month));
  97 + }
  98 + }
  99 +
  100 + List<CrmStatisticsPerformanceRespVO> computedList = new ArrayList<>();
  101 + List<CrmStatisticsPerformanceRespVO> respVOList = new ArrayList<>();
  102 +
  103 + // 生成computedList基础数据
  104 + // 构造完整的2*12个月的数据,如果某月数据缺失,需要补上0,一年12个月不能有缺失
  105 + for (String month : allMonths) {
  106 + CrmStatisticsPerformanceRespVO foundData = performance.stream()
  107 + .filter(data -> data.getTime().equals(month))
  108 + .findFirst()
  109 + .orElse(null);
  110 +
  111 + if (foundData != null) {
  112 + computedList.add(foundData);
  113 + } else {
  114 + CrmStatisticsPerformanceRespVO missingData = new CrmStatisticsPerformanceRespVO();
  115 + missingData.setTime(month);
  116 + missingData.setCurrentMonthCount(BigDecimal.ZERO);
  117 + missingData.setLastMonthCount(BigDecimal.ZERO);
  118 + missingData.setLastYearCount(BigDecimal.ZERO);
  119 + computedList.add(missingData);
  120 + }
  121 + }
  122 + //根据查询年份和前一年的数据,计算查询年份的同比环比数据
  123 + for (CrmStatisticsPerformanceRespVO currentData : computedList) {
  124 + String currentMonth = currentData.getTime();
  125 +
  126 + // 根据当年和前一年的月销售数据,计算currentYear的完整数据
  127 + if (currentMonth.startsWith(currentYear)) {
  128 + // 计算 LastMonthCount
  129 + int currentIndex = computedList.indexOf(currentData);
  130 + if (currentIndex > 0) {
  131 + CrmStatisticsPerformanceRespVO lastMonthData = computedList.get(currentIndex - 1);
  132 + currentData.setLastMonthCount(lastMonthData.getCurrentMonthCount());
  133 + } else {
  134 + currentData.setLastMonthCount(BigDecimal.ZERO); // 第一个月的 LastMonthCount 设为0
  135 + }
  136 +
  137 + // 计算 LastYearCount
  138 + String lastYearMonth = String.valueOf(Integer.parseInt(currentMonth) - 100);
  139 + CrmStatisticsPerformanceRespVO lastYearData = computedList.stream()
  140 + .filter(data -> data.getTime().equals(lastYearMonth))
  141 + .findFirst()
  142 + .orElse(null);
  143 +
  144 + if (lastYearData != null) {
  145 + currentData.setLastYearCount(lastYearData.getCurrentMonthCount());
  146 + } else {
  147 + currentData.setLastYearCount(BigDecimal.ZERO); // 如果去年同月数据不存在,设为0
  148 + }
  149 + respVOList.add(currentData);//给前端只需要返回查询当年的数据,不需要前一年数据
  150 + }
78 151 }
79   - return performance;
  152 + return respVOList;
80 153 }
81 154  
82 155 /**
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPortraitServiceImpl.java
... ... @@ -20,7 +20,6 @@ import java.util.Map;
20 20  
21 21 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
22 22 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
23   -import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
24 23  
25 24 /**
26 25 * CRM 客户画像 Service 实现类
... ... @@ -55,15 +54,18 @@ public class CrmStatisticsPortraitServiceImpl implements CrmStatisticsPortraitSe
55 54  
56 55 // 3. 拼接数据
57 56 List<Area> areaList = AreaUtils.getByType(AreaTypeEnum.PROVINCE, area -> area);
58   - areaList.add(new Area().setId(null).setName("未知")); // TODO @puhui999:是不是 65 find 的逻辑改下;不用 findAndThen,直接从 areaMap 拿;拿到就设置,不拿到就设置 null 和 未知;这样,58 本行可以删除掉完事了;这样代码更简单和一致
59 57 Map<Integer, Area> areaMap = convertMap(areaList, Area::getId);
60 58 return convertList(list, item -> {
61 59 Integer parentId = AreaUtils.getParentIdByType(item.getAreaId(), AreaTypeEnum.PROVINCE);
62   - if (parentId == null) { // 找不到,归到未知
63   - return item.setAreaId(null).setAreaName("未知");
  60 + if (parentId != null) {
  61 + Area area = areaMap.get(parentId);
  62 + if (area != null) {
  63 + item.setAreaId(parentId).setAreaName(area.getName());
  64 + return item;
  65 + }
64 66 }
65   - findAndThen(areaMap, parentId, area -> item.setAreaId(parentId).setAreaName(area.getName()));
66   - return item;
  67 + // 找不到,归到未知
  68 + return item.setAreaId(null).setAreaName("未知");
67 69 });
68 70 }
69 71  
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/util/CrmPermissionUtils.java
... ... @@ -64,8 +64,10 @@ public class CrmPermissionUtils {
64 64 }
65 65 // 2.2 场景二:我参与的数据
66 66 if (CrmSceneTypeEnum.isInvolved(sceneType)) {
67   - query.ne(ownerUserIdField, userId)
68   - .in(CrmPermissionDO::getLevel, CrmPermissionLevelEnum.READ.getLevel(), CrmPermissionLevelEnum.WRITE.getLevel());
  67 + query.innerJoin(CrmPermissionDO.class, on -> on.eq(CrmPermissionDO::getBizType, bizType)
  68 + .eq(CrmPermissionDO::getBizId, bizId)
  69 + .in(CrmPermissionDO::getLevel, CrmPermissionLevelEnum.READ.getLevel(), CrmPermissionLevelEnum.WRITE.getLevel()));
  70 + query.ne(ownerUserIdField, userId);
69 71 }
70 72 // 2.3 场景三:下属负责的数据
71 73 if (CrmSceneTypeEnum.isSubordinate(sceneType)) {
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsCustomerMapper.xml
... ... @@ -16,20 +16,20 @@
16 16 GROUP BY time
17 17 </select>
18 18  
19   - <!-- TODO 芋艿:应该不用过滤时间 -->
20 19 <select id="selectCustomerDealCountGroupByDate"
21 20 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByDateRespVO">
22   - SELECT DATE_FORMAT(customer.create_time, '%Y-%m-%d') AS time,
23   - COUNT(DISTINCT customer.id) AS customer_deal_count
  21 + SELECT DATE_FORMAT(customer.create_time, '%Y-%m-%d') AS time,
  22 + COUNT(DISTINCT customer.id) AS customer_deal_count
24 23 FROM crm_customer AS customer
25 24 LEFT JOIN crm_contract AS contract ON contract.customer_id = customer.id
26   - WHERE customer.deleted = 0 AND contract.deleted = 0
  25 + WHERE customer.deleted = 0
  26 + AND contract.deleted = 0
27 27 AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
28 28 AND customer.owner_user_id IN
29 29 <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
30 30 #{userId}
31 31 </foreach>
32   - AND contract.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
  32 + AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
33 33 GROUP BY time
34 34 </select>
35 35  
... ... @@ -53,13 +53,14 @@
53 53 COUNT(DISTINCT customer.id) AS customer_deal_count
54 54 FROM crm_customer AS customer
55 55 LEFT JOIN crm_contract AS contract ON contract.customer_id = customer.id
56   - WHERE customer.deleted = 0 AND contract.deleted = 0
  56 + WHERE customer.deleted = 0
  57 + AND contract.deleted = 0
57 58 AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
58 59 AND customer.owner_user_id IN
59 60 <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
60 61 #{userId}
61 62 </foreach>
62   - AND contract.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
  63 + AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
63 64 GROUP BY customer.owner_user_id
64 65 </select>
65 66  
... ... @@ -221,4 +222,45 @@
221 222 GROUP BY customer.owner_user_id
222 223 </select>
223 224  
  225 + <select id="selectCustomerDealCycleGroupByAreaId"
  226 + resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerDealCycleByAreaRespVO">
  227 + SELECT customer.area_id AS area_id,
  228 + IFNULL(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0) AS customer_deal_cycle,
  229 + COUNT(DISTINCT customer.id) AS customer_deal_count
  230 + FROM crm_customer AS customer
  231 + LEFT JOIN crm_contract AS contract ON customer.id = contract.customer_id
  232 + WHERE customer.deleted = 0
  233 + AND contract.deleted = 0
  234 + AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
  235 + AND customer.owner_user_id IN
  236 + <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
  237 + #{userId}
  238 + </foreach>
  239 + AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
  240 + GROUP BY
  241 + customer.area_id
  242 + </select>
  243 +
  244 + <select id="selectCustomerDealCycleGroupByProductId"
  245 + resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerDealCycleByProductRespVO">
  246 + SELECT product.name AS product_name,
  247 + IFNULL(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0) AS customer_deal_cycle,
  248 + COUNT(DISTINCT customer.id) AS customer_deal_count
  249 + FROM crm_customer AS customer
  250 + LEFT JOIN crm_contract AS contract ON customer.id = contract.customer_id
  251 + LEFT JOIN crm_contract_product AS contract_product ON contract_product.contract_id = contract.id
  252 + LEFT JOIN crm_product AS product ON contract_product.product_id = product.id
  253 + WHERE customer.deleted = 0
  254 + AND contract.deleted = 0
  255 + AND contract_product.deleted = 0
  256 + AND product.deleted = 0
  257 + AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
  258 + AND customer.owner_user_id IN
  259 + <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
  260 + #{userId}
  261 + </foreach>
  262 + AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
  263 + GROUP BY product.id
  264 + </select>
  265 +
224 266 </mapper>
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsFunnelMapper.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  3 +<mapper namespace="cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsFunnelMapper">
  4 +
  5 + <select id="selectCustomerCountByDate" resultType="java.lang.Long">
  6 + SELECT
  7 + COUNT(*)
  8 + FROM crm_customer
  9 + WHERE deleted = 0
  10 + AND owner_user_id IN
  11 + <!-- TODO @puhui999:这个 foreach 搞个缩进哈 - -->
  12 + <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
  13 + #{userId}
  14 + </foreach>
  15 + AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND
  16 + <!-- TODO @puhui999:下面这个,就不缩进啦 - -->
  17 + #{times[1],javaType=java.time.LocalDateTime}
  18 + </select>
  19 +
  20 + <select id="selectBusinessCountByDateAndEndStatus" resultType="java.lang.Long">
  21 + SELECT
  22 + COUNT(*)
  23 + FROM crm_business
  24 + WHERE deleted = 0
  25 + <if test="status != null">
  26 + AND end_status = #{status}
  27 + </if>
  28 + AND owner_user_id IN
  29 + <foreach collection="reqVO.userIds" item="userId" open="(" close=")" separator=",">
  30 + #{userId}
  31 + </foreach>
  32 + AND create_time BETWEEN #{reqVO.times[0],javaType=java.time.LocalDateTime} AND
  33 + #{reqVO.times[1],javaType=java.time.LocalDateTime}
  34 + </select>
  35 +
  36 + <select id="selectBusinessSummaryListGroupByEndStatus"
  37 + resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsBusinessSummaryByEndStatusRespVO">
  38 + SELECT
  39 + end_status AS endStatus,
  40 + COUNT(*) AS businessCount,
  41 + SUM(total_price) AS totalPrice
  42 + FROM crm_business
  43 + WHERE deleted = 0 AND end_status IS NOT NULL
  44 + AND owner_user_id IN
  45 + <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
  46 + #{userId}
  47 + </foreach>
  48 + AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND
  49 + #{times[1],javaType=java.time.LocalDateTime}
  50 + GROUP BY end_status
  51 + </select>
  52 +
  53 + <select id="selectBusinessSummaryGroupByDate"
  54 + resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsBusinessSummaryByDateRespVO">
  55 + SELECT
  56 + DATE_FORMAT(create_time, '%Y-%m-%d') AS time,
  57 + COUNT(*) AS businessCreateCount,
  58 + SUM(total_price) AS totalPrice
  59 + FROM crm_business
  60 + WHERE deleted = 0
  61 + AND owner_user_id IN
  62 + <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
  63 + #{userId}
  64 + </foreach>
  65 + AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND
  66 + #{times[1],javaType=java.time.LocalDateTime}
  67 + GROUP BY time
  68 + </select>
  69 +
  70 + <select id="selectBusinessInversionRateSummaryByDate"
  71 + resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsBusinessInversionRateSummaryByDateRespVO">
  72 + SELECT
  73 + DATE_FORMAT(create_time, '%Y-%m-%d') AS time,
  74 + COUNT(*) AS businessCount,
  75 + SUM(IF(end_status = 1, 1, 0)) AS businessWinCount
  76 + FROM crm_business
  77 + WHERE deleted = 0
  78 + AND owner_user_id IN
  79 + <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
  80 + #{userId}
  81 + </foreach>
  82 + AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND
  83 + #{times[1],javaType=java.time.LocalDateTime}
  84 + GROUP BY time
  85 + </select>
  86 +
  87 +</mapper>
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsPerformanceMapper.xml
... ... @@ -5,75 +5,28 @@
5 5 <select id="selectContractCountPerformance"
6 6 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO">
7 7 SELECT
8   - t.time as time,
9   - COALESCE(t.currentMonthCount,0) as currentMonthCount,
10   - COALESCE(y.lastMonthCount,0) as lastMonthCount,
11   - COALESCE(z.lastYearCount,0) as lastYearCount
12   - FROM
13   - (SELECT
14   - COUNT(1) AS currentMonthCount,
15   - DATE_FORMAT(order_date, '%Y-%m') AS time
16   - FROM crm_contract
  8 + DATE_FORMAT(order_date, '%Y%m') AS time,
  9 + COUNT(1) AS currentMonthCount
  10 + FROM crm_contract
17 11 WHERE deleted = 0
  12 + <!-- TODO @scholar:20 改成静态类引入 -->
18 13 AND audit_status = 20
19 14 AND owner_user_id in
20   - <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
21   - #{userId}
22   - </foreach>
23   - AND DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')
24   - GROUP BY time)t
25   - LEFT JOIN
26   - (SELECT
27   - COUNT(1) AS lastMonthCount,
28   - DATE_FORMAT(DATE_ADD(order_date,INTERVAL 1 MONTH), '%Y-%m') AS time
29   - FROM crm_contract
30   - WHERE deleted = 0
31   - AND audit_status = 20
32   - AND owner_user_id in
33   - <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
34   - #{userId}
35   - </foreach>
36   - AND (DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')
37   - or DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')-1)
38   - GROUP BY time)y ON t.time = y.time
39   - LEFT JOIN
40   - (SELECT
41   - COUNT(1) AS lastYearCount,
42   - DATE_FORMAT(DATE_ADD(order_date,INTERVAL 1 YEAR), '%Y-%m') AS time
43   - FROM crm_contract
44   - WHERE deleted = 0
45   - AND audit_status = 20
46   - AND owner_user_id in
47   - <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
48   - #{userId}
49   - </foreach>
50   - AND DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')-1
51   - GROUP BY time)z ON t.time = z.time
  15 + <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
  16 + #{userId}
  17 + </foreach>
  18 + <!-- TODO @scholar:CrmStatisticsPerformanceReqVO 传递 year,然后 java 代码里,转换出 times;这样,order_time 使用范围查询,避免使用函数 -->
  19 + AND (DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime}, '%Y')
  20 + or DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime}, '%Y') - 1)
  21 + GROUP BY time
52 22 </select>
  23 +
  24 + <!-- TODO @scholar:参考上面,调整下这个 SQL 的排版、和代码建议哈 -->
53 25 <select id="selectContractPricePerformance"
54 26 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO">
55 27 SELECT
56   - t.time as time,
57   - COALESCE(t.currentMonthCount,0) as currentMonthCount,
58   - COALESCE(y.lastMonthCount,0) as lastMonthCount,
59   - COALESCE(z.lastYearCount,0) as lastYearCount
60   - FROM
61   - (SELECT
62   - IFNULL(SUM(total_price), 0) AS currentMonthCount,
63   - DATE_FORMAT(order_date, '%Y-%m') AS time
64   - FROM crm_contract
65   - WHERE deleted = 0
66   - AND audit_status = 20
67   - AND owner_user_id in
68   - <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
69   - #{userId}
70   - </foreach>
71   - AND DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')
72   - GROUP BY time)t
73   - LEFT JOIN
74   - (SELECT
75   - IFNULL(SUM(total_price), 0) AS lastMonthCount,
76   - DATE_FORMAT(DATE_ADD(order_date,INTERVAL 1 MONTH), '%Y-%m') AS time
  28 + DATE_FORMAT(order_date, '%Y%m') AS time,
  29 + IFNULL(SUM(total_price), 0) AS currentMonthCount
77 30 FROM crm_contract
78 31 WHERE deleted = 0
79 32 AND audit_status = 20
... ... @@ -83,46 +36,15 @@
83 36 </foreach>
84 37 AND (DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')
85 38 or DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')-1)
86   - GROUP BY time)y ON t.time = y.time
87   - LEFT JOIN
88   - (SELECT
89   - IFNULL(SUM(total_price), 0) AS lastYearCount,
90   - DATE_FORMAT(DATE_ADD(order_date,INTERVAL 1 YEAR), '%Y-%m') AS time
91   - FROM crm_contract
92   - WHERE deleted = 0
93   - AND audit_status = 20
94   - AND owner_user_id in
95   - <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
96   - #{userId}
97   - </foreach>
98   - AND DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')-1
99   - GROUP BY time)z ON t.time = z.time
  39 + GROUP BY time
100 40 </select>
101 41  
  42 + <!-- TODO @scholar:参考上面,调整下这个 SQL 的排版、和代码建议哈 -->
102 43 <select id="selectReceivablePricePerformance"
103 44 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO">
104 45 SELECT
105   - t.time as time,
106   - COALESCE(t.currentMonthCount,0) as currentMonthCount,
107   - COALESCE(y.lastMonthCount,0) as lastMonthCount,
108   - COALESCE(z.lastYearCount,0) as lastYearCount
109   - FROM
110   - (SELECT
111   - IFNULL(SUM(price), 0) AS currentMonthCount,
112   - DATE_FORMAT(return_time, '%Y-%m') AS time
113   - FROM crm_receivable
114   - WHERE deleted = 0
115   - AND audit_status = 20
116   - AND owner_user_id in
117   - <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
118   - #{userId}
119   - </foreach>
120   - AND DATE_FORMAT(return_time, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')
121   - GROUP BY time)t
122   - LEFT JOIN
123   - (SELECT
124   - IFNULL(SUM(price), 0) AS lastMonthCount,
125   - DATE_FORMAT(DATE_ADD(return_time,INTERVAL 1 MONTH), '%Y-%m') AS time
  46 + DATE_FORMAT(return_time, '%Y%m') AS time,
  47 + IFNULL(SUM(price), 0) AS currentMonthCount
126 48 FROM crm_receivable
127 49 WHERE deleted = 0
128 50 AND audit_status = 20
... ... @@ -132,21 +54,7 @@
132 54 </foreach>
133 55 AND (DATE_FORMAT(return_time, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')
134 56 or DATE_FORMAT(return_time, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')-1)
135   - GROUP BY time)y ON t.time = y.time
136   - LEFT JOIN
137   - (SELECT
138   - IFNULL(SUM(price), 0) AS lastYearCount,
139   - DATE_FORMAT(DATE_ADD(return_time,INTERVAL 1 YEAR), '%Y-%m') AS time
140   - FROM crm_receivable
141   - WHERE deleted = 0
142   - AND audit_status = 20
143   - AND owner_user_id in
144   - <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
145   - #{userId}
146   - </foreach>
147   - AND DATE_FORMAT(return_time, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')-1
148   - GROUP BY time)z ON t.time = z.time
  57 + GROUP BY time
149 58 </select>
150 59  
151   -
152 60 </mapper>
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/db/DatabaseTableServiceImpl.java
... ... @@ -3,18 +3,22 @@ package cn.iocoder.yudao.module.infra.service.db;
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.lang.Assert;
5 5 import cn.hutool.core.util.StrUtil;
  6 +import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils;
6 7 import cn.iocoder.yudao.module.infra.dal.dataobject.db.DataSourceConfigDO;
  8 +import com.baomidou.mybatisplus.annotation.DbType;
7 9 import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
8 10 import com.baomidou.mybatisplus.generator.config.GlobalConfig;
9 11 import com.baomidou.mybatisplus.generator.config.StrategyConfig;
10 12 import com.baomidou.mybatisplus.generator.config.builder.ConfigBuilder;
11 13 import com.baomidou.mybatisplus.generator.config.po.TableInfo;
12 14 import com.baomidou.mybatisplus.generator.config.rules.DateType;
  15 +import com.baomidou.mybatisplus.generator.query.SQLQuery;
  16 +import jakarta.annotation.Resource;
13 17 import org.springframework.stereotype.Service;
14 18  
15   -import jakarta.annotation.Resource;
16 19 import java.util.Comparator;
17 20 import java.util.List;
  21 +import java.util.Objects;
18 22 import java.util.stream.Collectors;
19 23  
20 24 /**
... ... @@ -45,10 +49,14 @@ public class DatabaseTableServiceImpl implements DatabaseTableService {
45 49 // 获得数据源配置
46 50 DataSourceConfigDO config = dataSourceConfigService.getDataSourceConfig(dataSourceConfigId);
47 51 Assert.notNull(config, "数据源({}) 不存在!", dataSourceConfigId);
  52 + DbType dbType = JdbcUtils.getDbType(config.getUrl());
48 53  
49 54 // 使用 MyBatis Plus Generator 解析表结构
50   - DataSourceConfig dataSourceConfig = new DataSourceConfig.Builder(config.getUrl(), config.getUsername(),
51   - config.getPassword()).build();
  55 + DataSourceConfig.Builder dataSourceConfigBuilder = new DataSourceConfig.Builder(config.getUrl(), config.getUsername(),
  56 + config.getPassword());
  57 + if (Objects.equals(dbType, DbType.SQL_SERVER)) { // 特殊:SQLServer jdbc 非标准,参见 https://github.com/baomidou/mybatis-plus/issues/5419
  58 + dataSourceConfigBuilder.databaseQueryClass(SQLQuery.class);
  59 + }
52 60 StrategyConfig.Builder strategyConfig = new StrategyConfig.Builder().enableSkipView(); // 忽略视图,业务上一般用不到
53 61 if (StrUtil.isNotEmpty(name)) {
54 62 strategyConfig.addInclude(name);
... ... @@ -61,7 +69,7 @@ public class DatabaseTableServiceImpl implements DatabaseTableService {
61 69 }
62 70  
63 71 GlobalConfig globalConfig = new GlobalConfig.Builder().dateType(DateType.TIME_PACK).build(); // 只使用 LocalDateTime 类型,不使用 LocalDate
64   - ConfigBuilder builder = new ConfigBuilder(null, dataSourceConfig, strategyConfig.build(),
  72 + ConfigBuilder builder = new ConfigBuilder(null, dataSourceConfigBuilder.build(), strategyConfig.build(),
65 73 null, globalConfig, null);
66 74 // 按照名字排序
67 75 List<TableInfo> tables = builder.getTableInfoList();
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/comment/ProductCommentServiceImpl.java
... ... @@ -113,7 +113,7 @@ public class ProductCommentServiceImpl implements ProductCommentService {
113 113  
114 114 // 更新可见状态
115 115 productCommentMapper.updateById(new ProductCommentDO().setId(updateReqVO.getId())
116   - .setVisible(true));
  116 + .setVisible(updateReqVO.getVisible()));
117 117 }
118 118  
119 119 @Override
... ...
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImpl.java
... ... @@ -192,7 +192,9 @@ public class ProductSpuServiceImpl implements ProductSpuService {
192 192 if (CollUtil.isEmpty(ids)) {
193 193 return Collections.emptyList();
194 194 }
195   - return productSpuMapper.selectBatchIds(ids);
  195 + Map<Long, ProductSpuDO> spuMap = convertMap(productSpuMapper.selectBatchIds(ids), ProductSpuDO::getId);
  196 + // 需要按照 ids 顺序返回。例如说:店铺装修选择了 [3, 1, 2] 三个商品,返回结果还是 [3, 1, 2] 这样的顺序
  197 + return convertList(ids, spuMap::get);
196 198 }
197 199  
198 200 @Override
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java
... ... @@ -7,12 +7,12 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
7 7 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
8 8 import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.template.CouponTemplatePageReqVO;
9 9 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
  10 +import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTemplateValidityTypeEnum;
10 11 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
11 12 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
12 13 import org.apache.ibatis.annotations.Mapper;
13 14  
14 15 import java.time.LocalDateTime;
15   -import java.util.Collection;
16 16 import java.util.List;
17 17 import java.util.function.Consumer;
18 18  
... ... @@ -68,9 +68,9 @@ public interface CouponTemplateMapper extends BaseMapperX&lt;CouponTemplateDO&gt; {
68 68 canTakeConsumer = w ->
69 69 w.eq(CouponTemplateDO::getStatus, CommonStatusEnum.ENABLE.getStatus()) // 1. 状态为可用的
70 70 .in(CouponTemplateDO::getTakeType, canTakeTypes) // 2. 领取方式一致
71   - .and(ww -> ww.isNull(CouponTemplateDO::getValidEndTime) // 3. 未过期
72   - .or().gt(CouponTemplateDO::getValidEndTime, LocalDateTime.now()))
73   - .apply(" take_count < total_count "); // 4. 剩余数量大于 0
  71 + .and(ww -> ww.gt(CouponTemplateDO::getValidEndTime, LocalDateTime.now()) // 3.1 未过期
  72 + .or().eq(CouponTemplateDO::getValidityType, CouponTemplateValidityTypeEnum.TERM.getType())) // 3.2 领取之后
  73 + .apply(" (take_count < total_count OR total_count = -1 )"); // 4. 剩余数量大于 0,或者无限领取
74 74 }
75 75 return canTakeConsumer;
76 76 }
... ...
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/discount/DiscountActivityServiceImpl.java
... ... @@ -5,6 +5,7 @@ import cn.hutool.core.collection.CollectionUtil;
5 5 import cn.hutool.core.map.MapUtil;
6 6 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
7 7 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  8 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
8 9 import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityBaseVO;
9 10 import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityCreateReqVO;
10 11 import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityPageReqVO;
... ... @@ -18,6 +19,7 @@ import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnu
18 19 import cn.iocoder.yudao.module.promotion.util.PromotionUtils;
19 20 import jakarta.annotation.Resource;
20 21 import org.springframework.stereotype.Service;
  22 +import org.springframework.transaction.annotation.Transactional;
21 23 import org.springframework.validation.annotation.Validated;
22 24  
23 25 import java.time.LocalDateTime;
... ... @@ -48,31 +50,30 @@ public class DiscountActivityServiceImpl implements DiscountActivityService {
48 50  
49 51 @Override
50 52 public List<DiscountProductDO> getMatchDiscountProductList(Collection<Long> skuIds) {
51   - // TODO @zhangshuai:这里是不是可以直接 return discountProductMapper.getMatchDiscountProductList(skuIds); 一般来说,如果 idea 报“黄色”的警告,尽量都处理下哈;原则是,一切警告,皆为异常(错误),这样可以写出更好的代码。
52   - List<DiscountProductDO> matchDiscountProductList = discountProductMapper.getMatchDiscountProductList(skuIds);
53   - return matchDiscountProductList;
  53 + return discountProductMapper.getMatchDiscountProductList(skuIds);
54 54 }
55 55  
56 56 @Override
  57 + @Transactional(rollbackFor = Exception.class)
57 58 public Long createDiscountActivity(DiscountActivityCreateReqVO createReqVO) {
58 59 // 校验商品是否冲突
59 60 validateDiscountActivityProductConflicts(null, createReqVO.getProducts());
60 61  
61 62 // 插入活动
62 63 DiscountActivityDO discountActivity = DiscountActivityConvert.INSTANCE.convert(createReqVO)
63   - // TODO @zhangshuai:这里的调用去掉哈,强制就是开启的;
64   - .setStatus(PromotionUtils.calculateActivityStatus(createReqVO.getEndTime()));
  64 + .setStatus(CommonStatusEnum.ENABLE.getStatus());
65 65 discountActivityMapper.insert(discountActivity);
66 66 // 插入商品
67   - // TODO @zhangshuai:activityStatus 最好代码里,也做下设置噢。
68   - List<DiscountProductDO> discountProducts = convertList(createReqVO.getProducts(),
69   - product -> DiscountActivityConvert.INSTANCE.convert(product).setActivityId(discountActivity.getId()));
  67 + List<DiscountProductDO> discountProducts = BeanUtils.toBean(createReqVO.getProducts(), DiscountProductDO.class,
  68 + product -> product.setActivityId(discountActivity.getId()).setActivityStatus(discountActivity.getStatus())
  69 + .setActivityStartTime(createReqVO.getStartTime()).setActivityEndTime(createReqVO.getEndTime()));
70 70 discountProductMapper.insertBatch(discountProducts);
71 71 // 返回
72 72 return discountActivity.getId();
73 73 }
74 74  
75 75 @Override
  76 + @Transactional(rollbackFor = Exception.class)
76 77 public void updateDiscountActivity(DiscountActivityUpdateReqVO updateReqVO) {
77 78 // 校验存在
78 79 DiscountActivityDO discountActivity = validateDiscountActivityExists(updateReqVO.getId());
... ...
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/dal/mysql/trade/TradeOrderStatisticsMapper.java
... ... @@ -58,8 +58,8 @@ public interface TradeOrderStatisticsMapper extends BaseMapperX&lt;TradeStatisticsD
58 58  
59 59 Long selectCountByStatusAndDeliveryType(@Param("status") Integer status, @Param("deliveryType") Integer deliveryType);
60 60  
61   - TradeOrderSummaryRespVO selectPaySummaryByStatusAndPayTimeBetween(@Param("status") Integer status,
62   - @Param("beginTime") LocalDateTime beginTime,
63   - @Param("endTime") LocalDateTime endTime);
  61 + TradeOrderSummaryRespVO selectPaySummaryByPayStatusAndPayTimeBetween(@Param("payStatus") Integer payStatus,
  62 + @Param("beginTime") LocalDateTime beginTime,
  63 + @Param("endTime") LocalDateTime endTime);
64 64  
65 65 }
... ...
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/service/trade/TradeOrderStatisticsServiceImpl.java
... ... @@ -75,8 +75,8 @@ public class TradeOrderStatisticsServiceImpl implements TradeOrderStatisticsServ
75 75  
76 76 private TradeOrderSummaryRespVO getPayPriceSummary(LocalDateTime date) {
77 77 LocalDateTime beginTime = LocalDateTimeUtil.beginOfDay(date);
78   - LocalDateTime endTime = LocalDateTimeUtil.beginOfDay(date);
79   - return tradeOrderStatisticsMapper.selectPaySummaryByStatusAndPayTimeBetween(
  78 + LocalDateTime endTime = LocalDateTimeUtil.endOfDay(date);
  79 + return tradeOrderStatisticsMapper.selectPaySummaryByPayStatusAndPayTimeBetween(
80 80 PayOrderStatusEnum.SUCCESS.getStatus(), beginTime, endTime);
81 81 }
82 82  
... ...
yudao-module-mall/yudao-module-statistics-biz/src/main/resources/mapper/trade/TradeOrderStatisticsMapper.xml
... ... @@ -93,12 +93,12 @@
93 93 AND deleted = FALSE
94 94 </select>
95 95  
96   - <select id="selectPaySummaryByStatusAndPayTimeBetween"
  96 + <select id="selectPaySummaryByPayStatusAndPayTimeBetween"
97 97 resultType="cn.iocoder.yudao.module.statistics.controller.admin.trade.vo.TradeOrderSummaryRespVO">
98 98 SELECT IFNULL(SUM(pay_price), 0) AS orderPayPrice,
99 99 COUNT(1) AS orderPayCount
100 100 FROM trade_order
101   - WHERE status = #{status}
  101 + WHERE pay_status = #{payStatus}
102 102 AND pay_time BETWEEN #{beginTime} AND #{endTime}
103 103 AND deleted = FALSE
104 104 </select>
... ...
yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/order/TradeOrderOperateTypeEnum.java
... ... @@ -14,7 +14,7 @@ import lombok.RequiredArgsConstructor;
14 14 public enum TradeOrderOperateTypeEnum {
15 15  
16 16 MEMBER_CREATE(1, "用户下单"),
17   - ADMIN_UPDATE_PRICE(2, "订单价格 {oldPayPrice} 修改,实际支付金额为 {newPayPrice} 元"),
  17 + ADMIN_UPDATE_PRICE(2, "订单价格 {oldPayPrice} 修改,调整价格 {adjustPrice},实际支付金额为 {newPayPrice} 元"),
18 18 MEMBER_PAY(10, "用户付款成功"),
19 19 ADMIN_UPDATE_ADDRESS(11, "收货地址修改"),
20 20 ADMIN_DELIVERY(20, "已发货,快递公司:{deliveryName},快递单号:{logisticsNo}"),
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/delivery/DeliveryExpressTemplateServiceImpl.java
... ... @@ -88,7 +88,7 @@ public class DeliveryExpressTemplateServiceImpl implements DeliveryExpressTempla
88 88 List<DeliveryExpressTemplateFreeDO> oldList = expressTemplateFreeMapper.selectListByTemplateId(templateId);
89 89 List<DeliveryExpressTemplateFreeDO> newList = INSTANCE.convertTemplateFreeList(templateId, frees);
90 90 List<List<DeliveryExpressTemplateFreeDO>> diffList = CollectionUtils.diffList(oldList, newList,
91   - (oldVal, newVal) -> ObjectUtil.equal(oldVal.getId(), newVal.getTemplateId()));
  91 + (oldVal, newVal) -> ObjectUtil.equal(oldVal.getId(), newVal.getId()));
92 92  
93 93 // 第二步,批量添加、修改、删除
94 94 if (CollUtil.isNotEmpty(diffList.get(0))) {
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java
... ... @@ -631,7 +631,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
631 631  
632 632 // 2. 更新订单
633 633 tradeOrderMapper.updateById(new TradeOrderDO().setId(order.getId())
634   - .setAdjustPrice(reqVO.getAdjustPrice()).setPayPrice(newPayPrice));
  634 + .setAdjustPrice(reqVO.getAdjustPrice() + order.getAdjustPrice()).setPayPrice(newPayPrice));
635 635  
636 636 // 3. 更新 TradeOrderItem,需要做 adjustPrice 的分摊
637 637 List<TradeOrderItemDO> orderOrderItems = tradeOrderItemMapper.selectListByOrderId(order.getId());
... ... @@ -639,9 +639,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
639 639 List<TradeOrderItemDO> updateItems = new ArrayList<>();
640 640 for (int i = 0; i < orderOrderItems.size(); i++) {
641 641 TradeOrderItemDO item = orderOrderItems.get(i);
642   - // TODO puhui999: 已有分摊记录的情况下价格是否会不对,也就是说之前订单项 1 分摊了 10 块这次是 -100
643   - // 那么 setPayPrice 是否改为 (item.getPayPrice()-item.getAdjustPrice()) + dividePrices.get(i) 先减掉原来的价格再加上调价。经过验证可行,修改后订单价格增减都能正确分摊
644   - updateItems.add(new TradeOrderItemDO().setId(item.getId()).setAdjustPrice(dividePrices.get(i))
  642 + updateItems.add(new TradeOrderItemDO().setId(item.getId()).setAdjustPrice(item.getAdjustPrice() + dividePrices.get(i))
645 643 .setPayPrice((item.getPayPrice() - item.getAdjustPrice()) + dividePrices.get(i)));
646 644 }
647 645 tradeOrderItemMapper.updateBatch(updateItems);
... ... @@ -652,6 +650,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
652 650 // 5. 记录订单日志
653 651 TradeOrderLogUtils.setOrderInfo(order.getId(), order.getStatus(), order.getStatus(),
654 652 MapUtil.<String, Object>builder().put("oldPayPrice", MoneyUtils.fenToYuanStr(order.getPayPrice()))
  653 + .put("adjustPrice", MoneyUtils.fenToYuanStr(reqVO.getAdjustPrice()))
655 654 .put("newPayPrice", MoneyUtils.fenToYuanStr(newPayPrice)).build());
656 655 }
657 656  
... ...
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java
... ... @@ -245,7 +245,7 @@ public class TradePriceCalculatorHelper {
245 245 * @return 分摊金额数组,和传入的 orderItems 一一对应
246 246 */
247 247 public static List<Integer> dividePrice2(List<TradeOrderItemDO> items, Integer price) {
248   - Integer total = getSumValue(items, TradeOrderItemDO::getPrice, Integer::sum);
  248 + Integer total = getSumValue(items, TradeOrderItemDO::getPayPrice, Integer::sum);
249 249 assert total != null;
250 250 // 遍历每一个,进行分摊
251 251 List<Integer> prices = new ArrayList<>(items.size());
... ... @@ -254,15 +254,11 @@ public class TradePriceCalculatorHelper {
254 254 TradeOrderItemDO orderItem = items.get(i);
255 255 int partPrice;
256 256 if (i < items.size() - 1) { // 减一的原因,是因为拆分时,如果按照比例,可能会出现.所以最后一个,使用反减
257   - // partPrice = (int) (price * (1.0D * orderItem.getPayPrice() / total));
258   - // pr fix: 改为了使用订单原价来计算比例
259 257 partPrice = (int) (price * (1.0D * orderItem.getPrice() / total));
260 258 remainPrice -= partPrice;
261 259 } else {
262 260 partPrice = remainPrice;
263 261 }
264   - // TODO puhui999: 如果是减价的情况这里过不了
265   - // Assert.isTrue(partPrice >= 0, "分摊金额必须大于等于 0");
266 262 prices.add(partPrice);
267 263 }
268 264 return prices;
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java
... ... @@ -36,13 +36,15 @@ public class PayWalletServiceImpl implements PayWalletService {
36 36  
37 37 @Resource
38 38 private PayWalletMapper walletMapper;
  39 +
39 40 @Resource
  41 + @Lazy // 延迟加载,避免循环依赖
40 42 private PayWalletTransactionService walletTransactionService;
41 43 @Resource
42   - @Lazy
  44 + @Lazy // 延迟加载,避免循环依赖
43 45 private PayOrderService orderService;
44 46 @Resource
45   - @Lazy
  47 + @Lazy // 延迟加载,避免循环依赖
46 48 private PayRefundService refundService;
47 49  
48 50 @Override
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptRespVO.java
... ... @@ -18,7 +18,7 @@ public class DeptRespVO {
18 18 @Schema(description = "父部门 ID", example = "1024")
19 19 private Long parentId;
20 20  
21   - @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  21 + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
22 22 private Integer sort;
23 23  
24 24 @Schema(description = "负责人的用户编号", example = "2048")
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptSaveReqVO.java
... ... @@ -25,7 +25,7 @@ public class DeptSaveReqVO {
25 25 @Schema(description = "父部门 ID", example = "1024")
26 26 private Long parentId;
27 27  
28   - @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  28 + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
29 29 @NotNull(message = "显示顺序不能为空")
30 30 private Integer sort;
31 31  
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostRespVO.java
... ... @@ -27,7 +27,7 @@ public class PostRespVO {
27 27 @ExcelProperty("岗位编码")
28 28 private String code;
29 29  
30   - @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  30 + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
31 31 @ExcelProperty("岗位排序")
32 32 private Integer sort;
33 33  
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostSaveReqVO.java
... ... @@ -26,7 +26,7 @@ public class PostSaveReqVO {
26 26 @Size(max = 64, message = "岗位编码长度不能超过64个字符")
27 27 private String code;
28 28  
29   - @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  29 + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
30 30 @NotNull(message = "显示顺序不能为空")
31 31 private Integer sort;
32 32  
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataRespVO.java
... ... @@ -19,7 +19,7 @@ public class DictDataRespVO {
19 19 @ExcelProperty("字典编码")
20 20 private Long id;
21 21  
22   - @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  22 + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
23 23 @ExcelProperty("字典排序")
24 24 private Integer sort;
25 25  
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataSaveReqVO.java
... ... @@ -16,7 +16,7 @@ public class DictDataSaveReqVO {
16 16 @Schema(description = "字典数据编号", example = "1024")
17 17 private Long id;
18 18  
19   - @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  19 + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
20 20 @NotNull(message = "显示顺序不能为空")
21 21 private Integer sort;
22 22  
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuRespVO.java
... ... @@ -28,7 +28,7 @@ public class MenuRespVO {
28 28 @NotNull(message = "菜单类型不能为空")
29 29 private Integer type;
30 30  
31   - @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  31 + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
32 32 @NotNull(message = "显示顺序不能为空")
33 33 private Integer sort;
34 34  
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuSaveVO.java
... ... @@ -27,7 +27,7 @@ public class MenuSaveVO {
27 27 @NotNull(message = "菜单类型不能为空")
28 28 private Integer type;
29 29  
30   - @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  30 + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
31 31 @NotNull(message = "显示顺序不能为空")
32 32 private Integer sort;
33 33  
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java
... ... @@ -30,7 +30,7 @@ public class RoleRespVO {
30 30 @ExcelProperty("角色标志")
31 31 private String code;
32 32  
33   - @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  33 + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
34 34 @ExcelProperty("角色排序")
35 35 private Integer sort;
36 36  
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleSaveReqVO.java
... ... @@ -27,7 +27,7 @@ public class RoleSaveReqVO {
27 27 @DiffLogField(name = "角色标志")
28 28 private String code;
29 29  
30   - @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  30 + @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
31 31 @NotNull(message = "显示顺序不能为空")
32 32 @DiffLogField(name = "显示顺序")
33 33 private Integer sort;
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java
... ... @@ -2,14 +2,16 @@ package cn.iocoder.yudao.module.system.service.permission;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
5   -import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuSaveVO;
6 5 import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuListReqVO;
  6 +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuSaveVO;
7 7 import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
8 8 import cn.iocoder.yudao.module.system.dal.mysql.permission.MenuMapper;
9 9 import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants;
10 10 import cn.iocoder.yudao.module.system.enums.permission.MenuTypeEnum;
11 11 import cn.iocoder.yudao.module.system.service.tenant.TenantService;
12 12 import com.google.common.annotations.VisibleForTesting;
  13 +import com.google.common.collect.Lists;
  14 +import jakarta.annotation.Resource;
13 15 import lombok.extern.slf4j.Slf4j;
14 16 import org.springframework.cache.annotation.CacheEvict;
15 17 import org.springframework.cache.annotation.Cacheable;
... ... @@ -17,7 +19,6 @@ import org.springframework.context.annotation.Lazy;
17 19 import org.springframework.stereotype.Service;
18 20 import org.springframework.transaction.annotation.Transactional;
19 21  
20   -import jakarta.annotation.Resource;
21 22 import java.util.Collection;
22 23 import java.util.List;
23 24  
... ... @@ -130,6 +131,10 @@ public class MenuServiceImpl implements MenuService {
130 131  
131 132 @Override
132 133 public List<MenuDO> getMenuList(Collection<Long> ids) {
  134 + // 当 ids 为空时,返回一个空的实例对象
  135 + if (CollUtil.isEmpty(ids)) {
  136 + return Lists.newArrayList();
  137 + }
133 138 return menuMapper.selectBatchIds(ids);
134 139 }
135 140  
... ...
yudao-server/src/main/resources/application-dev.yaml
... ... @@ -188,6 +188,16 @@ justauth:
188 188 client-secret: 1wTb7hYxnpT2TUbIeHGXGo7T0odav1ic10mLdyyATOw
189 189 agent-id: 1000004
190 190 ignore-check-redirect-uri: true
  191 + # noinspection SpringBootApplicationYaml
  192 + WECHAT_MINI_APP: # 微信小程序
  193 + client-id: ${wx.miniapp.appid}
  194 + client-secret: ${wx.miniapp.secret}
  195 + ignore-check-redirect-uri: true
  196 + ignore-check-state: true # 微信小程序,不会使用到 state,所以不进行校验
  197 + WECHAT_MP: # 微信公众号
  198 + client-id: ${wx.mp.app-id}
  199 + client-secret: ${wx.mp.secret}
  200 + ignore-check-redirect-uri: true
191 201 cache:
192 202 type: REDIS
193 203 prefix: 'social_auth_state:' # 缓存前缀,目前只对 Redis 缓存生效,默认 JUSTAUTH::STATE::
... ...
yudao-server/src/main/resources/application-local.yaml
... ... @@ -243,6 +243,7 @@ justauth:
243 243 client-secret: 1wTb7hYxnpT2TUbIeHGXGo7T0odav1ic10mLdyyATOw
244 244 agent-id: 1000004
245 245 ignore-check-redirect-uri: true
  246 + # noinspection SpringBootApplicationYaml
246 247 WECHAT_MINI_APP: # 微信小程序
247 248 client-id: ${wx.miniapp.appid}
248 249 client-secret: ${wx.miniapp.secret}
... ...