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