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,7 +31,7 @@
31 <redisson.version>3.26.0</redisson.version> 31 <redisson.version>3.26.0</redisson.version>
32 <dm8.jdbc.version>8.1.3.62</dm8.jdbc.version> 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 <lock4j.version>2.2.7</lock4j.version> 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,6 +97,10 @@ public class CollectionUtils {
97 .collect(Collectors.toList()); 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 public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func) { 104 public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func) {
101 if (CollUtil.isEmpty(from)) { 105 if (CollUtil.isEmpty(from)) {
102 return new HashSet<>(); 106 return new HashSet<>();
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/rabbitmq/config/YudaoRabbitMQAutoConfiguration.java
1 package cn.iocoder.yudao.framework.mq.rabbitmq.config; 1 package cn.iocoder.yudao.framework.mq.rabbitmq.config;
2 2
3 -import cn.hutool.core.util.ReflectUtil;  
4 import lombok.extern.slf4j.Slf4j; 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 import org.springframework.boot.autoconfigure.AutoConfiguration; 6 import org.springframework.boot.autoconfigure.AutoConfiguration;
7 import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 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 * RabbitMQ 消息队列配置类 11 * RabbitMQ 消息队列配置类
@@ -18,12 +17,12 @@ import java.lang.reflect.Field; @@ -18,12 +17,12 @@ import java.lang.reflect.Field;
18 @ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate") 17 @ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate")
19 public class YudaoRabbitMQAutoConfiguration { 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 \ No newline at end of file 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 \ No newline at end of file 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,7 +177,7 @@ public class CrmBusinessController {
177 buildBusinessDetailList(list)); 177 buildBusinessDetailList(list));
178 } 178 }
179 179
180 - private List<CrmBusinessRespVO> buildBusinessDetailList(List<CrmBusinessDO> list) { 180 + public List<CrmBusinessRespVO> buildBusinessDetailList(List<CrmBusinessDO> list) {
181 if (CollUtil.isEmpty(list)) { 181 if (CollUtil.isEmpty(list)) {
182 return Collections.emptyList(); 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,3 +53,13 @@ tenant-id: {{adminTenentId}}
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 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 Authorization: Bearer {{token}} 54 Authorization: Bearer {{token}}
55 tenant-id: {{adminTenentId}} 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,6 +96,18 @@ public class CrmStatisticsCustomerController {
96 return success(customerService.getCustomerDealCycleByUser(reqVO)); 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,6 +2,7 @@ package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.portrait;
2 2
3 import io.swagger.v3.oas.annotations.media.Schema; 3 import io.swagger.v3.oas.annotations.media.Schema;
4 import jakarta.validation.constraints.NotNull; 4 import jakarta.validation.constraints.NotNull;
  5 +import jakarta.validation.constraints.Size;
5 import lombok.Data; 6 import lombok.Data;
6 import org.springframework.format.annotation.DateTimeFormat; 7 import org.springframework.format.annotation.DateTimeFormat;
7 8
@@ -31,12 +32,9 @@ public class CrmStatisticsPortraitReqVO { @@ -31,12 +32,9 @@ public class CrmStatisticsPortraitReqVO {
31 @Schema(description = "负责人用户 id 集合", hidden = true, example = "2") 32 @Schema(description = "负责人用户 id 集合", hidden = true, example = "2")
32 private List<Long> userIds; 33 private List<Long> userIds;
33 34
34 - /**  
35 - * 前端如果选择自定义时间, 那么前端传递起始-终止时间, 如果选择其他时间间隔类型, 则由后台计算起始-终止时间  
36 - * 并作为参数传递给Mapper  
37 - */  
38 @Schema(description = "时间范围", requiredMode = Schema.RequiredMode.NOT_REQUIRED) 35 @Schema(description = "时间范围", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
39 @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) 36 @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  37 + @Size(min = 2, max = 2, message = "请选择时间范围")
40 private LocalDateTime[] times; 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,8 +5,8 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6 import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX; 6 import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
7 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessPageReqVO; 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 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO; 9 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
9 -import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;  
10 import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum; 10 import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
11 import cn.iocoder.yudao.module.crm.util.CrmPermissionUtils; 11 import cn.iocoder.yudao.module.crm.util.CrmPermissionUtils;
12 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; 12 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
@@ -59,10 +59,16 @@ public interface CrmBusinessMapper extends BaseMapperX&lt;CrmBusinessDO&gt; { @@ -59,10 +59,16 @@ public interface CrmBusinessMapper extends BaseMapperX&lt;CrmBusinessDO&gt; {
59 return selectCount(CrmBusinessDO::getStatusTypeId, statusTypeId); 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 return selectList(new LambdaQueryWrapperX<CrmBusinessDO>() 63 return selectList(new LambdaQueryWrapperX<CrmBusinessDO>()
64 .eq(CrmBusinessDO::getCustomerId, customerId) 64 .eq(CrmBusinessDO::getCustomerId, customerId)
65 .eq(CrmBusinessDO::getOwnerUserId, ownerUserId)); 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,6 +53,7 @@ public interface CrmStatisticsCustomerMapper {
53 53
54 /** 54 /**
55 * 合同总金额(按用户) 55 * 合同总金额(按用户)
  56 + *
56 * @return 统计数据@return 统计数据@param reqVO 请求参数 57 * @return 统计数据@return 统计数据@param reqVO 请求参数
57 * @return 统计数据 58 * @return 统计数据
58 */ 59 */
@@ -191,4 +192,20 @@ public interface CrmStatisticsCustomerMapper { @@ -191,4 +192,20 @@ public interface CrmStatisticsCustomerMapper {
191 */ 192 */
192 List<CrmStatisticsCustomerDealCycleByUserRespVO> selectCustomerDealCycleGroupByUser(CrmStatisticsCustomerReqVO reqVO); 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,6 +5,7 @@ import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusi
5 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessSaveReqVO; 5 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessSaveReqVO;
6 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessTransferReqVO; 6 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessTransferReqVO;
7 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessUpdateStatusReqVO; 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 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO; 9 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
9 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessProductDO; 10 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessProductDO;
10 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessStatusDO; 11 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessStatusDO;
@@ -194,4 +195,12 @@ public interface CrmBusinessService { @@ -194,4 +195,12 @@ public interface CrmBusinessService {
194 */ 195 */
195 List<CrmBusinessDO> getBusinessListByCustomerIdOwnerUserId(Long customerId, Long ownerUserId); 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,6 +10,7 @@ import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusi
10 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessTransferReqVO; 10 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessTransferReqVO;
11 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessUpdateStatusReqVO; 11 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessUpdateStatusReqVO;
12 import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.CrmContactBusinessReqVO; 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 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO; 14 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
14 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessProductDO; 15 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessProductDO;
15 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessStatusDO; 16 import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessStatusDO;
@@ -375,4 +376,9 @@ public class CrmBusinessServiceImpl implements CrmBusinessService { @@ -375,4 +376,9 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
375 return businessMapper.selectListByCustomerIdOwnerUserId(customerId, ownerUserId); 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,7 +120,7 @@ public class CrmClueServiceImpl implements CrmClueService {
120 } 120 }
121 121
122 @Override 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 success = CRM_CLUE_FOLLOW_UP_SUCCESS) 124 success = CRM_CLUE_FOLLOW_UP_SUCCESS)
125 @CrmPermission(bizType = CrmBizTypeEnum.CRM_CLUE, bizId = "#id", level = CrmPermissionLevelEnum.WRITE) 125 @CrmPermission(bizType = CrmBizTypeEnum.CRM_CLUE, bizId = "#id", level = CrmPermissionLevelEnum.WRITE)
126 public void updateClueFollowUp(Long id, LocalDateTime contactNextTime, String contactLastContent) { 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,7 +39,7 @@ public interface CrmCustomerService {
39 /** 39 /**
40 * 更新客户的跟进状态 40 * 更新客户的跟进状态
41 * 41 *
42 - * @param id 编号 42 + * @param id 编号
43 * @param dealStatus 跟进状态 43 * @param dealStatus 跟进状态
44 */ 44 */
45 void updateCustomerDealStatus(Long id, Boolean dealStatus); 45 void updateCustomerDealStatus(Long id, Boolean dealStatus);
@@ -47,8 +47,8 @@ public interface CrmCustomerService { @@ -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 * @param contactLastContent 最后联系内容 52 * @param contactLastContent 最后联系内容
53 */ 53 */
54 void updateCustomerFollowUp(Long id, LocalDateTime contactNextTime, String contactLastContent); 54 void updateCustomerFollowUp(Long id, LocalDateTime contactNextTime, String contactLastContent);
@@ -99,8 +99,8 @@ public interface CrmCustomerService { @@ -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 * @return 客户分页 104 * @return 客户分页
105 */ 105 */
106 PageResult<CrmCustomerDO> getPutPoolRemindCustomerPage(CrmCustomerPageReqVO pageVO, Long userId); 106 PageResult<CrmCustomerDO> getPutPoolRemindCustomerPage(CrmCustomerPageReqVO pageVO, Long userId);
@@ -108,7 +108,7 @@ public interface CrmCustomerService { @@ -108,7 +108,7 @@ public interface CrmCustomerService {
108 /** 108 /**
109 * 获得待进入公海的客户数量 109 * 获得待进入公海的客户数量
110 * 110 *
111 - * @param userId 用户编号 111 + * @param userId 用户编号
112 * @return 提醒数量 112 * @return 提醒数量
113 */ 113 */
114 Long getPutPoolRemindCustomerCount(Long userId); 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,6 +47,7 @@ import java.time.LocalDateTime;
47 import java.util.*; 47 import java.util.*;
48 48
49 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 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 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; 51 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
51 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*; 52 import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
52 import static cn.iocoder.yudao.module.crm.enums.LogRecordConstants.*; 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,4 +93,20 @@ public interface CrmStatisticsCustomerService {
93 */ 93 */
94 List<CrmStatisticsCustomerDealCycleByUserRespVO> getCustomerDealCycleByUser(CrmStatisticsCustomerReqVO reqVO); 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,6 +4,9 @@ import cn.hutool.core.collection.CollUtil;
4 import cn.hutool.core.util.ObjUtil; 4 import cn.hutool.core.util.ObjUtil;
5 import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; 5 import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
6 import cn.iocoder.yudao.framework.common.util.number.NumberUtils; 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 import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.*; 10 import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.*;
8 import cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsCustomerMapper; 11 import cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsCustomerMapper;
9 import cn.iocoder.yudao.module.system.api.dept.DeptApi; 12 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
@@ -19,6 +22,7 @@ import java.time.LocalDateTime; @@ -19,6 +22,7 @@ import java.time.LocalDateTime;
19 import java.util.Collections; 22 import java.util.Collections;
20 import java.util.List; 23 import java.util.List;
21 import java.util.Map; 24 import java.util.Map;
  25 +import java.util.function.Function;
22 import java.util.stream.Stream; 26 import java.util.stream.Stream;
23 27
24 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; 28 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
@@ -290,6 +294,46 @@ public class CrmStatisticsCustomerServiceImpl implements CrmStatisticsCustomerSe @@ -290,6 +294,46 @@ public class CrmStatisticsCustomerServiceImpl implements CrmStatisticsCustomerSe
290 return summaryList; 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 package cn.iocoder.yudao.module.crm.service.statistics; 1 package cn.iocoder.yudao.module.crm.service.statistics;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.date.LocalDateTimeUtil;
4 import cn.hutool.core.util.ObjUtil; 5 import cn.hutool.core.util.ObjUtil;
5 import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceReqVO; 6 import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceReqVO;
6 import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO; 7 import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO;
@@ -13,8 +14,9 @@ import org.springframework.stereotype.Service; @@ -13,8 +14,9 @@ import org.springframework.stereotype.Service;
13 import org.springframework.validation.annotation.Validated; 14 import org.springframework.validation.annotation.Validated;
14 15
15 import jakarta.annotation.Resource; 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 import java.util.function.Function; 20 import java.util.function.Function;
19 21
20 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; 22 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@@ -38,7 +40,7 @@ public class CrmStatisticsPerformanceServiceImpl implements CrmStatisticsPerform @@ -38,7 +40,7 @@ public class CrmStatisticsPerformanceServiceImpl implements CrmStatisticsPerform
38 40
39 @Override 41 @Override
40 public List<CrmStatisticsPerformanceRespVO> getContractCountPerformance(CrmStatisticsPerformanceReqVO performanceReqVO) { 42 public List<CrmStatisticsPerformanceRespVO> getContractCountPerformance(CrmStatisticsPerformanceReqVO performanceReqVO) {
41 - // TODO @scholar:我们可以换个思路实现,减少数据库的计算量; 43 + // TODO @scholar:可以把下面这个注释,你理解后,重新整理下,写到 getPerformance 里;
42 // 比如说,2024 年的合同数据,是不是 2022-12 到 2024-12-31,每个月的统计呢? 44 // 比如说,2024 年的合同数据,是不是 2022-12 到 2024-12-31,每个月的统计呢?
43 // 理解之后,我们可以数据 group by 年-月,20222-12 到 2024-12-31 的,然后内存在聚合出 CrmStatisticsPerformanceRespVO 这样 45 // 理解之后,我们可以数据 group by 年-月,20222-12 到 2024-12-31 的,然后内存在聚合出 CrmStatisticsPerformanceRespVO 这样
44 // 这样,我们就可以减少数据库的计算量,提升性能;同时 SQL 也会很简单,开发者理解起来也简单哈; 46 // 这样,我们就可以减少数据库的计算量,提升性能;同时 SQL 也会很简单,开发者理解起来也简单哈;
@@ -55,28 +57,99 @@ public class CrmStatisticsPerformanceServiceImpl implements CrmStatisticsPerform @@ -55,28 +57,99 @@ public class CrmStatisticsPerformanceServiceImpl implements CrmStatisticsPerform
55 return getPerformance(performanceReqVO, performanceMapper::selectReceivablePricePerformance); 57 return getPerformance(performanceReqVO, performanceMapper::selectReceivablePricePerformance);
56 } 58 }
57 59
  60 + // TODO @scholar:代码注释,应该有 3 个变量哈;
58 /** 61 /**
59 * 获得员工业绩数据 62 * 获得员工业绩数据
60 * 63 *
61 * @param performanceReqVO 参数 64 * @param performanceReqVO 参数
62 - * @param performanceFunction 排行榜方法  
63 - * @return 排行版数据 65 + * @param performanceFunction 员工业绩统计方法
  66 + * @return 员工业绩数据
64 */ 67 */
  68 + // TODO @scholar:下面一行的变量,超过一行了,阅读不美观;可以考虑每一行一个变量;
65 private List<CrmStatisticsPerformanceRespVO> getPerformance(CrmStatisticsPerformanceReqVO performanceReqVO, Function<CrmStatisticsPerformanceReqVO, 69 private List<CrmStatisticsPerformanceRespVO> getPerformance(CrmStatisticsPerformanceReqVO performanceReqVO, Function<CrmStatisticsPerformanceReqVO,
66 List<CrmStatisticsPerformanceRespVO>> performanceFunction) { 70 List<CrmStatisticsPerformanceRespVO>> performanceFunction) {
67 71
  72 + // TODO @scholar:没使用到的变量,建议删除;
  73 + List<CrmStatisticsPerformanceRespVO> performanceRespVOList;
  74 +
68 // 1. 获得用户编号数组 75 // 1. 获得用户编号数组
69 final List<Long> userIds = getUserIds(performanceReqVO); 76 final List<Long> userIds = getUserIds(performanceReqVO);
70 if (CollUtil.isEmpty(userIds)) { 77 if (CollUtil.isEmpty(userIds)) {
71 return Collections.emptyList(); 78 return Collections.emptyList();
72 } 79 }
73 performanceReqVO.setUserIds(userIds); 80 performanceReqVO.setUserIds(userIds);
74 - // 2. 获得排行数据 81 + // TODO @scholar:1. 和 2. 之间,可以考虑换一行;保证每一块逻辑的间隔;
  82 + // 2. 获得业绩数据
  83 + // TODO @scholar:复数变量,建议使用 s 或者 list 结果;这里用 performanceList 好列;
75 List<CrmStatisticsPerformanceRespVO> performance = performanceFunction.apply(performanceReqVO); 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,7 +20,6 @@ import java.util.Map;
20 20
21 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; 21 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
22 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; 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 * CRM 客户画像 Service 实现类 25 * CRM 客户画像 Service 实现类
@@ -55,15 +54,18 @@ public class CrmStatisticsPortraitServiceImpl implements CrmStatisticsPortraitSe @@ -55,15 +54,18 @@ public class CrmStatisticsPortraitServiceImpl implements CrmStatisticsPortraitSe
55 54
56 // 3. 拼接数据 55 // 3. 拼接数据
57 List<Area> areaList = AreaUtils.getByType(AreaTypeEnum.PROVINCE, area -> area); 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 Map<Integer, Area> areaMap = convertMap(areaList, Area::getId); 57 Map<Integer, Area> areaMap = convertMap(areaList, Area::getId);
60 return convertList(list, item -> { 58 return convertList(list, item -> {
61 Integer parentId = AreaUtils.getParentIdByType(item.getAreaId(), AreaTypeEnum.PROVINCE); 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,8 +64,10 @@ public class CrmPermissionUtils {
64 } 64 }
65 // 2.2 场景二:我参与的数据 65 // 2.2 场景二:我参与的数据
66 if (CrmSceneTypeEnum.isInvolved(sceneType)) { 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 // 2.3 场景三:下属负责的数据 72 // 2.3 场景三:下属负责的数据
71 if (CrmSceneTypeEnum.isSubordinate(sceneType)) { 73 if (CrmSceneTypeEnum.isSubordinate(sceneType)) {
yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsCustomerMapper.xml
@@ -16,20 +16,20 @@ @@ -16,20 +16,20 @@
16 GROUP BY time 16 GROUP BY time
17 </select> 17 </select>
18 18
19 - <!-- TODO 芋艿:应该不用过滤时间 -->  
20 <select id="selectCustomerDealCountGroupByDate" 19 <select id="selectCustomerDealCountGroupByDate"
21 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByDateRespVO"> 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 FROM crm_customer AS customer 23 FROM crm_customer AS customer
25 LEFT JOIN crm_contract AS contract ON contract.customer_id = customer.id 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 AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} 27 AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
28 AND customer.owner_user_id IN 28 AND customer.owner_user_id IN
29 <foreach collection="userIds" item="userId" open="(" close=")" separator=","> 29 <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
30 #{userId} 30 #{userId}
31 </foreach> 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 GROUP BY time 33 GROUP BY time
34 </select> 34 </select>
35 35
@@ -53,13 +53,14 @@ @@ -53,13 +53,14 @@
53 COUNT(DISTINCT customer.id) AS customer_deal_count 53 COUNT(DISTINCT customer.id) AS customer_deal_count
54 FROM crm_customer AS customer 54 FROM crm_customer AS customer
55 LEFT JOIN crm_contract AS contract ON contract.customer_id = customer.id 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 AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} 58 AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
58 AND customer.owner_user_id IN 59 AND customer.owner_user_id IN
59 <foreach collection="userIds" item="userId" open="(" close=")" separator=","> 60 <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
60 #{userId} 61 #{userId}
61 </foreach> 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 GROUP BY customer.owner_user_id 64 GROUP BY customer.owner_user_id
64 </select> 65 </select>
65 66
@@ -221,4 +222,45 @@ @@ -221,4 +222,45 @@
221 GROUP BY customer.owner_user_id 222 GROUP BY customer.owner_user_id
222 </select> 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 </mapper> 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,75 +5,28 @@
5 <select id="selectContractCountPerformance" 5 <select id="selectContractCountPerformance"
6 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO"> 6 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO">
7 SELECT 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 WHERE deleted = 0 11 WHERE deleted = 0
  12 + <!-- TODO @scholar:20 改成静态类引入 -->
18 AND audit_status = 20 13 AND audit_status = 20
19 AND owner_user_id in 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 </select> 22 </select>
  23 +
  24 + <!-- TODO @scholar:参考上面,调整下这个 SQL 的排版、和代码建议哈 -->
53 <select id="selectContractPricePerformance" 25 <select id="selectContractPricePerformance"
54 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO"> 26 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO">
55 SELECT 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 FROM crm_contract 30 FROM crm_contract
78 WHERE deleted = 0 31 WHERE deleted = 0
79 AND audit_status = 20 32 AND audit_status = 20
@@ -83,46 +36,15 @@ @@ -83,46 +36,15 @@
83 </foreach> 36 </foreach>
84 AND (DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y') 37 AND (DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')
85 or DATE_FORMAT(order_date, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')-1) 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 </select> 40 </select>
101 41
  42 + <!-- TODO @scholar:参考上面,调整下这个 SQL 的排版、和代码建议哈 -->
102 <select id="selectReceivablePricePerformance" 43 <select id="selectReceivablePricePerformance"
103 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO"> 44 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO">
104 SELECT 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 FROM crm_receivable 48 FROM crm_receivable
127 WHERE deleted = 0 49 WHERE deleted = 0
128 AND audit_status = 20 50 AND audit_status = 20
@@ -132,21 +54,7 @@ @@ -132,21 +54,7 @@
132 </foreach> 54 </foreach>
133 AND (DATE_FORMAT(return_time, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y') 55 AND (DATE_FORMAT(return_time, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')
134 or DATE_FORMAT(return_time, '%Y') = DATE_FORMAT(#{times[0],javaType=java.time.LocalDateTime},'%Y')-1) 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 </select> 58 </select>
150 59
151 -  
152 </mapper> 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,18 +3,22 @@ package cn.iocoder.yudao.module.infra.service.db;
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 import cn.hutool.core.lang.Assert; 4 import cn.hutool.core.lang.Assert;
5 import cn.hutool.core.util.StrUtil; 5 import cn.hutool.core.util.StrUtil;
  6 +import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils;
6 import cn.iocoder.yudao.module.infra.dal.dataobject.db.DataSourceConfigDO; 7 import cn.iocoder.yudao.module.infra.dal.dataobject.db.DataSourceConfigDO;
  8 +import com.baomidou.mybatisplus.annotation.DbType;
7 import com.baomidou.mybatisplus.generator.config.DataSourceConfig; 9 import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
8 import com.baomidou.mybatisplus.generator.config.GlobalConfig; 10 import com.baomidou.mybatisplus.generator.config.GlobalConfig;
9 import com.baomidou.mybatisplus.generator.config.StrategyConfig; 11 import com.baomidou.mybatisplus.generator.config.StrategyConfig;
10 import com.baomidou.mybatisplus.generator.config.builder.ConfigBuilder; 12 import com.baomidou.mybatisplus.generator.config.builder.ConfigBuilder;
11 import com.baomidou.mybatisplus.generator.config.po.TableInfo; 13 import com.baomidou.mybatisplus.generator.config.po.TableInfo;
12 import com.baomidou.mybatisplus.generator.config.rules.DateType; 14 import com.baomidou.mybatisplus.generator.config.rules.DateType;
  15 +import com.baomidou.mybatisplus.generator.query.SQLQuery;
  16 +import jakarta.annotation.Resource;
13 import org.springframework.stereotype.Service; 17 import org.springframework.stereotype.Service;
14 18
15 -import jakarta.annotation.Resource;  
16 import java.util.Comparator; 19 import java.util.Comparator;
17 import java.util.List; 20 import java.util.List;
  21 +import java.util.Objects;
18 import java.util.stream.Collectors; 22 import java.util.stream.Collectors;
19 23
20 /** 24 /**
@@ -45,10 +49,14 @@ public class DatabaseTableServiceImpl implements DatabaseTableService { @@ -45,10 +49,14 @@ public class DatabaseTableServiceImpl implements DatabaseTableService {
45 // 获得数据源配置 49 // 获得数据源配置
46 DataSourceConfigDO config = dataSourceConfigService.getDataSourceConfig(dataSourceConfigId); 50 DataSourceConfigDO config = dataSourceConfigService.getDataSourceConfig(dataSourceConfigId);
47 Assert.notNull(config, "数据源({}) 不存在!", dataSourceConfigId); 51 Assert.notNull(config, "数据源({}) 不存在!", dataSourceConfigId);
  52 + DbType dbType = JdbcUtils.getDbType(config.getUrl());
48 53
49 // 使用 MyBatis Plus Generator 解析表结构 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 StrategyConfig.Builder strategyConfig = new StrategyConfig.Builder().enableSkipView(); // 忽略视图,业务上一般用不到 60 StrategyConfig.Builder strategyConfig = new StrategyConfig.Builder().enableSkipView(); // 忽略视图,业务上一般用不到
53 if (StrUtil.isNotEmpty(name)) { 61 if (StrUtil.isNotEmpty(name)) {
54 strategyConfig.addInclude(name); 62 strategyConfig.addInclude(name);
@@ -61,7 +69,7 @@ public class DatabaseTableServiceImpl implements DatabaseTableService { @@ -61,7 +69,7 @@ public class DatabaseTableServiceImpl implements DatabaseTableService {
61 } 69 }
62 70
63 GlobalConfig globalConfig = new GlobalConfig.Builder().dateType(DateType.TIME_PACK).build(); // 只使用 LocalDateTime 类型,不使用 LocalDate 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 null, globalConfig, null); 73 null, globalConfig, null);
66 // 按照名字排序 74 // 按照名字排序
67 List<TableInfo> tables = builder.getTableInfoList(); 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,7 +113,7 @@ public class ProductCommentServiceImpl implements ProductCommentService {
113 113
114 // 更新可见状态 114 // 更新可见状态
115 productCommentMapper.updateById(new ProductCommentDO().setId(updateReqVO.getId()) 115 productCommentMapper.updateById(new ProductCommentDO().setId(updateReqVO.getId())
116 - .setVisible(true)); 116 + .setVisible(updateReqVO.getVisible()));
117 } 117 }
118 118
119 @Override 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,7 +192,9 @@ public class ProductSpuServiceImpl implements ProductSpuService {
192 if (CollUtil.isEmpty(ids)) { 192 if (CollUtil.isEmpty(ids)) {
193 return Collections.emptyList(); 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 @Override 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,12 +7,12 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
7 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 7 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
8 import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.template.CouponTemplatePageReqVO; 8 import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.template.CouponTemplatePageReqVO;
9 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO; 9 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
  10 +import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTemplateValidityTypeEnum;
10 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 11 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
11 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; 12 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
12 import org.apache.ibatis.annotations.Mapper; 13 import org.apache.ibatis.annotations.Mapper;
13 14
14 import java.time.LocalDateTime; 15 import java.time.LocalDateTime;
15 -import java.util.Collection;  
16 import java.util.List; 16 import java.util.List;
17 import java.util.function.Consumer; 17 import java.util.function.Consumer;
18 18
@@ -68,9 +68,9 @@ public interface CouponTemplateMapper extends BaseMapperX&lt;CouponTemplateDO&gt; { @@ -68,9 +68,9 @@ public interface CouponTemplateMapper extends BaseMapperX&lt;CouponTemplateDO&gt; {
68 canTakeConsumer = w -> 68 canTakeConsumer = w ->
69 w.eq(CouponTemplateDO::getStatus, CommonStatusEnum.ENABLE.getStatus()) // 1. 状态为可用的 69 w.eq(CouponTemplateDO::getStatus, CommonStatusEnum.ENABLE.getStatus()) // 1. 状态为可用的
70 .in(CouponTemplateDO::getTakeType, canTakeTypes) // 2. 领取方式一致 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 return canTakeConsumer; 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,6 +5,7 @@ import cn.hutool.core.collection.CollectionUtil;
5 import cn.hutool.core.map.MapUtil; 5 import cn.hutool.core.map.MapUtil;
6 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 6 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
7 import cn.iocoder.yudao.framework.common.pojo.PageResult; 7 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  8 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
8 import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityBaseVO; 9 import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityBaseVO;
9 import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityCreateReqVO; 10 import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityCreateReqVO;
10 import cn.iocoder.yudao.module.promotion.controller.admin.discount.vo.DiscountActivityPageReqVO; 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,6 +19,7 @@ import cn.iocoder.yudao.module.promotion.enums.common.PromotionActivityStatusEnu
18 import cn.iocoder.yudao.module.promotion.util.PromotionUtils; 19 import cn.iocoder.yudao.module.promotion.util.PromotionUtils;
19 import jakarta.annotation.Resource; 20 import jakarta.annotation.Resource;
20 import org.springframework.stereotype.Service; 21 import org.springframework.stereotype.Service;
  22 +import org.springframework.transaction.annotation.Transactional;
21 import org.springframework.validation.annotation.Validated; 23 import org.springframework.validation.annotation.Validated;
22 24
23 import java.time.LocalDateTime; 25 import java.time.LocalDateTime;
@@ -48,31 +50,30 @@ public class DiscountActivityServiceImpl implements DiscountActivityService { @@ -48,31 +50,30 @@ public class DiscountActivityServiceImpl implements DiscountActivityService {
48 50
49 @Override 51 @Override
50 public List<DiscountProductDO> getMatchDiscountProductList(Collection<Long> skuIds) { 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 @Override 56 @Override
  57 + @Transactional(rollbackFor = Exception.class)
57 public Long createDiscountActivity(DiscountActivityCreateReqVO createReqVO) { 58 public Long createDiscountActivity(DiscountActivityCreateReqVO createReqVO) {
58 // 校验商品是否冲突 59 // 校验商品是否冲突
59 validateDiscountActivityProductConflicts(null, createReqVO.getProducts()); 60 validateDiscountActivityProductConflicts(null, createReqVO.getProducts());
60 61
61 // 插入活动 62 // 插入活动
62 DiscountActivityDO discountActivity = DiscountActivityConvert.INSTANCE.convert(createReqVO) 63 DiscountActivityDO discountActivity = DiscountActivityConvert.INSTANCE.convert(createReqVO)
63 - // TODO @zhangshuai:这里的调用去掉哈,强制就是开启的;  
64 - .setStatus(PromotionUtils.calculateActivityStatus(createReqVO.getEndTime())); 64 + .setStatus(CommonStatusEnum.ENABLE.getStatus());
65 discountActivityMapper.insert(discountActivity); 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 discountProductMapper.insertBatch(discountProducts); 70 discountProductMapper.insertBatch(discountProducts);
71 // 返回 71 // 返回
72 return discountActivity.getId(); 72 return discountActivity.getId();
73 } 73 }
74 74
75 @Override 75 @Override
  76 + @Transactional(rollbackFor = Exception.class)
76 public void updateDiscountActivity(DiscountActivityUpdateReqVO updateReqVO) { 77 public void updateDiscountActivity(DiscountActivityUpdateReqVO updateReqVO) {
77 // 校验存在 78 // 校验存在
78 DiscountActivityDO discountActivity = validateDiscountActivityExists(updateReqVO.getId()); 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,8 +58,8 @@ public interface TradeOrderStatisticsMapper extends BaseMapperX&lt;TradeStatisticsD
58 58
59 Long selectCountByStatusAndDeliveryType(@Param("status") Integer status, @Param("deliveryType") Integer deliveryType); 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,8 +75,8 @@ public class TradeOrderStatisticsServiceImpl implements TradeOrderStatisticsServ
75 75
76 private TradeOrderSummaryRespVO getPayPriceSummary(LocalDateTime date) { 76 private TradeOrderSummaryRespVO getPayPriceSummary(LocalDateTime date) {
77 LocalDateTime beginTime = LocalDateTimeUtil.beginOfDay(date); 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 PayOrderStatusEnum.SUCCESS.getStatus(), beginTime, endTime); 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,12 +93,12 @@
93 AND deleted = FALSE 93 AND deleted = FALSE
94 </select> 94 </select>
95 95
96 - <select id="selectPaySummaryByStatusAndPayTimeBetween" 96 + <select id="selectPaySummaryByPayStatusAndPayTimeBetween"
97 resultType="cn.iocoder.yudao.module.statistics.controller.admin.trade.vo.TradeOrderSummaryRespVO"> 97 resultType="cn.iocoder.yudao.module.statistics.controller.admin.trade.vo.TradeOrderSummaryRespVO">
98 SELECT IFNULL(SUM(pay_price), 0) AS orderPayPrice, 98 SELECT IFNULL(SUM(pay_price), 0) AS orderPayPrice,
99 COUNT(1) AS orderPayCount 99 COUNT(1) AS orderPayCount
100 FROM trade_order 100 FROM trade_order
101 - WHERE status = #{status} 101 + WHERE pay_status = #{payStatus}
102 AND pay_time BETWEEN #{beginTime} AND #{endTime} 102 AND pay_time BETWEEN #{beginTime} AND #{endTime}
103 AND deleted = FALSE 103 AND deleted = FALSE
104 </select> 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,7 +14,7 @@ import lombok.RequiredArgsConstructor;
14 public enum TradeOrderOperateTypeEnum { 14 public enum TradeOrderOperateTypeEnum {
15 15
16 MEMBER_CREATE(1, "用户下单"), 16 MEMBER_CREATE(1, "用户下单"),
17 - ADMIN_UPDATE_PRICE(2, "订单价格 {oldPayPrice} 修改,实际支付金额为 {newPayPrice} 元"), 17 + ADMIN_UPDATE_PRICE(2, "订单价格 {oldPayPrice} 修改,调整价格 {adjustPrice},实际支付金额为 {newPayPrice} 元"),
18 MEMBER_PAY(10, "用户付款成功"), 18 MEMBER_PAY(10, "用户付款成功"),
19 ADMIN_UPDATE_ADDRESS(11, "收货地址修改"), 19 ADMIN_UPDATE_ADDRESS(11, "收货地址修改"),
20 ADMIN_DELIVERY(20, "已发货,快递公司:{deliveryName},快递单号:{logisticsNo}"), 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,7 +88,7 @@ public class DeliveryExpressTemplateServiceImpl implements DeliveryExpressTempla
88 List<DeliveryExpressTemplateFreeDO> oldList = expressTemplateFreeMapper.selectListByTemplateId(templateId); 88 List<DeliveryExpressTemplateFreeDO> oldList = expressTemplateFreeMapper.selectListByTemplateId(templateId);
89 List<DeliveryExpressTemplateFreeDO> newList = INSTANCE.convertTemplateFreeList(templateId, frees); 89 List<DeliveryExpressTemplateFreeDO> newList = INSTANCE.convertTemplateFreeList(templateId, frees);
90 List<List<DeliveryExpressTemplateFreeDO>> diffList = CollectionUtils.diffList(oldList, newList, 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 if (CollUtil.isNotEmpty(diffList.get(0))) { 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,7 +631,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
631 631
632 // 2. 更新订单 632 // 2. 更新订单
633 tradeOrderMapper.updateById(new TradeOrderDO().setId(order.getId()) 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 // 3. 更新 TradeOrderItem,需要做 adjustPrice 的分摊 636 // 3. 更新 TradeOrderItem,需要做 adjustPrice 的分摊
637 List<TradeOrderItemDO> orderOrderItems = tradeOrderItemMapper.selectListByOrderId(order.getId()); 637 List<TradeOrderItemDO> orderOrderItems = tradeOrderItemMapper.selectListByOrderId(order.getId());
@@ -639,9 +639,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { @@ -639,9 +639,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
639 List<TradeOrderItemDO> updateItems = new ArrayList<>(); 639 List<TradeOrderItemDO> updateItems = new ArrayList<>();
640 for (int i = 0; i < orderOrderItems.size(); i++) { 640 for (int i = 0; i < orderOrderItems.size(); i++) {
641 TradeOrderItemDO item = orderOrderItems.get(i); 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 .setPayPrice((item.getPayPrice() - item.getAdjustPrice()) + dividePrices.get(i))); 643 .setPayPrice((item.getPayPrice() - item.getAdjustPrice()) + dividePrices.get(i)));
646 } 644 }
647 tradeOrderItemMapper.updateBatch(updateItems); 645 tradeOrderItemMapper.updateBatch(updateItems);
@@ -652,6 +650,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { @@ -652,6 +650,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
652 // 5. 记录订单日志 650 // 5. 记录订单日志
653 TradeOrderLogUtils.setOrderInfo(order.getId(), order.getStatus(), order.getStatus(), 651 TradeOrderLogUtils.setOrderInfo(order.getId(), order.getStatus(), order.getStatus(),
654 MapUtil.<String, Object>builder().put("oldPayPrice", MoneyUtils.fenToYuanStr(order.getPayPrice())) 652 MapUtil.<String, Object>builder().put("oldPayPrice", MoneyUtils.fenToYuanStr(order.getPayPrice()))
  653 + .put("adjustPrice", MoneyUtils.fenToYuanStr(reqVO.getAdjustPrice()))
655 .put("newPayPrice", MoneyUtils.fenToYuanStr(newPayPrice)).build()); 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,7 +245,7 @@ public class TradePriceCalculatorHelper {
245 * @return 分摊金额数组,和传入的 orderItems 一一对应 245 * @return 分摊金额数组,和传入的 orderItems 一一对应
246 */ 246 */
247 public static List<Integer> dividePrice2(List<TradeOrderItemDO> items, Integer price) { 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 assert total != null; 249 assert total != null;
250 // 遍历每一个,进行分摊 250 // 遍历每一个,进行分摊
251 List<Integer> prices = new ArrayList<>(items.size()); 251 List<Integer> prices = new ArrayList<>(items.size());
@@ -254,15 +254,11 @@ public class TradePriceCalculatorHelper { @@ -254,15 +254,11 @@ public class TradePriceCalculatorHelper {
254 TradeOrderItemDO orderItem = items.get(i); 254 TradeOrderItemDO orderItem = items.get(i);
255 int partPrice; 255 int partPrice;
256 if (i < items.size() - 1) { // 减一的原因,是因为拆分时,如果按照比例,可能会出现.所以最后一个,使用反减 256 if (i < items.size() - 1) { // 减一的原因,是因为拆分时,如果按照比例,可能会出现.所以最后一个,使用反减
257 - // partPrice = (int) (price * (1.0D * orderItem.getPayPrice() / total));  
258 - // pr fix: 改为了使用订单原价来计算比例  
259 partPrice = (int) (price * (1.0D * orderItem.getPrice() / total)); 257 partPrice = (int) (price * (1.0D * orderItem.getPrice() / total));
260 remainPrice -= partPrice; 258 remainPrice -= partPrice;
261 } else { 259 } else {
262 partPrice = remainPrice; 260 partPrice = remainPrice;
263 } 261 }
264 - // TODO puhui999: 如果是减价的情况这里过不了  
265 - // Assert.isTrue(partPrice >= 0, "分摊金额必须大于等于 0");  
266 prices.add(partPrice); 262 prices.add(partPrice);
267 } 263 }
268 return prices; 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,13 +36,15 @@ public class PayWalletServiceImpl implements PayWalletService {
36 36
37 @Resource 37 @Resource
38 private PayWalletMapper walletMapper; 38 private PayWalletMapper walletMapper;
  39 +
39 @Resource 40 @Resource
  41 + @Lazy // 延迟加载,避免循环依赖
40 private PayWalletTransactionService walletTransactionService; 42 private PayWalletTransactionService walletTransactionService;
41 @Resource 43 @Resource
42 - @Lazy 44 + @Lazy // 延迟加载,避免循环依赖
43 private PayOrderService orderService; 45 private PayOrderService orderService;
44 @Resource 46 @Resource
45 - @Lazy 47 + @Lazy // 延迟加载,避免循环依赖
46 private PayRefundService refundService; 48 private PayRefundService refundService;
47 49
48 @Override 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,7 +18,7 @@ public class DeptRespVO {
18 @Schema(description = "父部门 ID", example = "1024") 18 @Schema(description = "父部门 ID", example = "1024")
19 private Long parentId; 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 private Integer sort; 22 private Integer sort;
23 23
24 @Schema(description = "负责人的用户编号", example = "2048") 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,7 +25,7 @@ public class DeptSaveReqVO {
25 @Schema(description = "父部门 ID", example = "1024") 25 @Schema(description = "父部门 ID", example = "1024")
26 private Long parentId; 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 @NotNull(message = "显示顺序不能为空") 29 @NotNull(message = "显示顺序不能为空")
30 private Integer sort; 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,7 +27,7 @@ public class PostRespVO {
27 @ExcelProperty("岗位编码") 27 @ExcelProperty("岗位编码")
28 private String code; 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 @ExcelProperty("岗位排序") 31 @ExcelProperty("岗位排序")
32 private Integer sort; 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,7 +26,7 @@ public class PostSaveReqVO {
26 @Size(max = 64, message = "岗位编码长度不能超过64个字符") 26 @Size(max = 64, message = "岗位编码长度不能超过64个字符")
27 private String code; 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 @NotNull(message = "显示顺序不能为空") 30 @NotNull(message = "显示顺序不能为空")
31 private Integer sort; 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,7 +19,7 @@ public class DictDataRespVO {
19 @ExcelProperty("字典编码") 19 @ExcelProperty("字典编码")
20 private Long id; 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 @ExcelProperty("字典排序") 23 @ExcelProperty("字典排序")
24 private Integer sort; 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,7 +16,7 @@ public class DictDataSaveReqVO {
16 @Schema(description = "字典数据编号", example = "1024") 16 @Schema(description = "字典数据编号", example = "1024")
17 private Long id; 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 @NotNull(message = "显示顺序不能为空") 20 @NotNull(message = "显示顺序不能为空")
21 private Integer sort; 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,7 +28,7 @@ public class MenuRespVO {
28 @NotNull(message = "菜单类型不能为空") 28 @NotNull(message = "菜单类型不能为空")
29 private Integer type; 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 @NotNull(message = "显示顺序不能为空") 32 @NotNull(message = "显示顺序不能为空")
33 private Integer sort; 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,7 +27,7 @@ public class MenuSaveVO {
27 @NotNull(message = "菜单类型不能为空") 27 @NotNull(message = "菜单类型不能为空")
28 private Integer type; 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 @NotNull(message = "显示顺序不能为空") 31 @NotNull(message = "显示顺序不能为空")
32 private Integer sort; 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,7 +30,7 @@ public class RoleRespVO {
30 @ExcelProperty("角色标志") 30 @ExcelProperty("角色标志")
31 private String code; 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 @ExcelProperty("角色排序") 34 @ExcelProperty("角色排序")
35 private Integer sort; 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,7 +27,7 @@ public class RoleSaveReqVO {
27 @DiffLogField(name = "角色标志") 27 @DiffLogField(name = "角色标志")
28 private String code; 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 @NotNull(message = "显示顺序不能为空") 31 @NotNull(message = "显示顺序不能为空")
32 @DiffLogField(name = "显示顺序") 32 @DiffLogField(name = "显示顺序")
33 private Integer sort; 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,14 +2,16 @@ package cn.iocoder.yudao.module.system.service.permission;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 4 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
5 -import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuSaveVO;  
6 import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuListReqVO; 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 import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; 7 import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
8 import cn.iocoder.yudao.module.system.dal.mysql.permission.MenuMapper; 8 import cn.iocoder.yudao.module.system.dal.mysql.permission.MenuMapper;
9 import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants; 9 import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants;
10 import cn.iocoder.yudao.module.system.enums.permission.MenuTypeEnum; 10 import cn.iocoder.yudao.module.system.enums.permission.MenuTypeEnum;
11 import cn.iocoder.yudao.module.system.service.tenant.TenantService; 11 import cn.iocoder.yudao.module.system.service.tenant.TenantService;
12 import com.google.common.annotations.VisibleForTesting; 12 import com.google.common.annotations.VisibleForTesting;
  13 +import com.google.common.collect.Lists;
  14 +import jakarta.annotation.Resource;
13 import lombok.extern.slf4j.Slf4j; 15 import lombok.extern.slf4j.Slf4j;
14 import org.springframework.cache.annotation.CacheEvict; 16 import org.springframework.cache.annotation.CacheEvict;
15 import org.springframework.cache.annotation.Cacheable; 17 import org.springframework.cache.annotation.Cacheable;
@@ -17,7 +19,6 @@ import org.springframework.context.annotation.Lazy; @@ -17,7 +19,6 @@ import org.springframework.context.annotation.Lazy;
17 import org.springframework.stereotype.Service; 19 import org.springframework.stereotype.Service;
18 import org.springframework.transaction.annotation.Transactional; 20 import org.springframework.transaction.annotation.Transactional;
19 21
20 -import jakarta.annotation.Resource;  
21 import java.util.Collection; 22 import java.util.Collection;
22 import java.util.List; 23 import java.util.List;
23 24
@@ -130,6 +131,10 @@ public class MenuServiceImpl implements MenuService { @@ -130,6 +131,10 @@ public class MenuServiceImpl implements MenuService {
130 131
131 @Override 132 @Override
132 public List<MenuDO> getMenuList(Collection<Long> ids) { 133 public List<MenuDO> getMenuList(Collection<Long> ids) {
  134 + // 当 ids 为空时,返回一个空的实例对象
  135 + if (CollUtil.isEmpty(ids)) {
  136 + return Lists.newArrayList();
  137 + }
133 return menuMapper.selectBatchIds(ids); 138 return menuMapper.selectBatchIds(ids);
134 } 139 }
135 140
yudao-server/src/main/resources/application-dev.yaml
@@ -188,6 +188,16 @@ justauth: @@ -188,6 +188,16 @@ justauth:
188 client-secret: 1wTb7hYxnpT2TUbIeHGXGo7T0odav1ic10mLdyyATOw 188 client-secret: 1wTb7hYxnpT2TUbIeHGXGo7T0odav1ic10mLdyyATOw
189 agent-id: 1000004 189 agent-id: 1000004
190 ignore-check-redirect-uri: true 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 cache: 201 cache:
192 type: REDIS 202 type: REDIS
193 prefix: 'social_auth_state:' # 缓存前缀,目前只对 Redis 缓存生效,默认 JUSTAUTH::STATE:: 203 prefix: 'social_auth_state:' # 缓存前缀,目前只对 Redis 缓存生效,默认 JUSTAUTH::STATE::
yudao-server/src/main/resources/application-local.yaml
@@ -243,6 +243,7 @@ justauth: @@ -243,6 +243,7 @@ justauth:
243 client-secret: 1wTb7hYxnpT2TUbIeHGXGo7T0odav1ic10mLdyyATOw 243 client-secret: 1wTb7hYxnpT2TUbIeHGXGo7T0odav1ic10mLdyyATOw
244 agent-id: 1000004 244 agent-id: 1000004
245 ignore-check-redirect-uri: true 245 ignore-check-redirect-uri: true
  246 + # noinspection SpringBootApplicationYaml
246 WECHAT_MINI_APP: # 微信小程序 247 WECHAT_MINI_APP: # 微信小程序
247 client-id: ${wx.miniapp.appid} 248 client-id: ${wx.miniapp.appid}
248 client-secret: ${wx.miniapp.secret} 249 client-secret: ${wx.miniapp.secret}