diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml
index e25014d41..7d8c900e6 100644
--- a/yudao-dependencies/pom.xml
+++ b/yudao-dependencies/pom.xml
@@ -31,7 +31,7 @@
3.26.0
8.1.3.62
- 2.2.3
+ 2.3.0
2.2.7
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
index 0d06bc799..91f534788 100644
--- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
@@ -97,6 +97,10 @@ public class CollectionUtils {
.collect(Collectors.toList());
}
+ public static Set convertSet(Collection from) {
+ return convertSet(from, v -> v);
+ }
+
public static Set convertSet(Collection from, Function func) {
if (CollUtil.isEmpty(from)) {
return new HashSet<>();
diff --git a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/rabbitmq/config/YudaoRabbitMQAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/rabbitmq/config/YudaoRabbitMQAutoConfiguration.java
index 770c50ff7..af1467376 100644
--- a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/rabbitmq/config/YudaoRabbitMQAutoConfiguration.java
+++ b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/rabbitmq/config/YudaoRabbitMQAutoConfiguration.java
@@ -1,12 +1,11 @@
package cn.iocoder.yudao.framework.mq.rabbitmq.config;
-import cn.hutool.core.util.ReflectUtil;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.amqp.utils.SerializationUtils;
+import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
+import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
-
-import java.lang.reflect.Field;
+import org.springframework.context.annotation.Bean;
/**
* RabbitMQ 消息队列配置类
@@ -18,12 +17,12 @@ import java.lang.reflect.Field;
@ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate")
public class YudaoRabbitMQAutoConfiguration {
- static {
- // 强制设置 SerializationUtils 的 TRUST_ALL 为 true,避免 RabbitMQ Consumer 反序列化消息报错
- // 为什么不通过设置 spring.amqp.deserialization.trust.all 呢?因为可能在 SerializationUtils static 初始化后
- Field trustAllField = ReflectUtil.getField(SerializationUtils.class, "TRUST_ALL");
- ReflectUtil.removeFinalModify(trustAllField);
- ReflectUtil.setFieldValue(SerializationUtils.class, trustAllField, true);
+ /**
+ * Jackson2JsonMessageConverter Bean:使用 jackson 序列化消息
+ */
+ @Bean
+ public MessageConverter createMessageConverter() {
+ return new Jackson2JsonMessageConverter();
}
}
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
index 0ea434134..f7bc24223 100644
--- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
@@ -1 +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.
*
* HistoricProcessInstance & ProcessInstance 的关系:
* 1.
*
* 简单来说,前者 = 历史 + 运行中的流程实例,后者仅是运行中的流程实例
*
* @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 getProcessInstances(Set ids) {
return runtimeService.createProcessInstanceQuery().processInstanceIds(ids).list();
}
@Override
public HistoricProcessInstance getHistoricProcessInstance(String id) {
return historyService.createHistoricProcessInstanceQuery().processInstanceId(id).includeProcessVariables().singleResult();
}
@Override
public List getHistoricProcessInstances(Set ids) {
return historyService.createHistoricProcessInstanceQuery().processInstanceIds(ids).list();
}
@Override
public PageResult 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 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 variables, String businessKey,
Map> 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> startUserSelectAssignees) {
// 1. 获得发起人自选审批人的 UserTask 列表
BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(definition.getId());
List userTaskList = BpmTaskCandidateStartUserSelectStrategy.getStartUserSelectUserTaskList(bpmnModel);
if (CollUtil.isEmpty(userTaskList)) {
return;
}
// 2. 校验发起人自选审批人的 UserTask 是否都配置了
userTaskList.forEach(userTask -> {
List 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 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);
}
}
\ No newline at end of file
+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.
*
* HistoricProcessInstance & ProcessInstance 的关系:
* 1.
*
* 简单来说,前者 = 历史 + 运行中的流程实例,后者仅是运行中的流程实例
*
* @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 getProcessInstances(Set ids) {
return runtimeService.createProcessInstanceQuery().processInstanceIds(ids).list();
}
@Override
public HistoricProcessInstance getHistoricProcessInstance(String id) {
return historyService.createHistoricProcessInstanceQuery().processInstanceId(id).includeProcessVariables().singleResult();
}
@Override
public List getHistoricProcessInstances(Set ids) {
return historyService.createHistoricProcessInstanceQuery().processInstanceIds(ids).list();
}
@Override
public PageResult 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 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 variables, String businessKey,
Map> 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> startUserSelectAssignees) {
// 1. 获得发起人自选审批人的 UserTask 列表
BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(definition.getId());
List userTaskList = BpmTaskCandidateStartUserSelectStrategy.getStartUserSelectUserTaskList(bpmnModel);
if (CollUtil.isEmpty(userTaskList)) {
return;
}
// 2. 校验发起人自选审批人的 UserTask 是否都配置了
userTaskList.forEach(userTask -> {
List 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 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);
}
}
\ No newline at end of file
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
index a2bbd0636..7c2dae223 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
+++ b/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 {
buildBusinessDetailList(list));
}
- private List buildBusinessDetailList(List list) {
+ public List buildBusinessDetailList(List list) {
if (CollUtil.isEmpty(list)) {
return Collections.emptyList();
}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsCustomerController.http b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsCustomerController.http
index 389bf4ac9..6b960512d 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsCustomerController.http
+++ b/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}}
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
Authorization: Bearer {{token}}
tenant-id: {{adminTenentId}}
+
+### 6.3 获取客户成交周期(按区域)
+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
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+### 6.4 获取客户成交周期(按产品)
+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
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsCustomerController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsCustomerController.java
index 51d149900..3539b5db5 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsCustomerController.java
+++ b/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 {
return success(customerService.getCustomerDealCycleByUser(reqVO));
}
- // TODO dhb52:【成交周期分析】里,有按照员工(已实现)、地区(未实现)、产品(未实现),需要在看看哈;可以把 CustomerDealCycle 拆成 3 个 tab,员工客户成交周期分析、地区客户成交周期分析、产品客户成交周期分析;
+ @GetMapping("/get-customer-deal-cycle-by-area")
+ @Operation(summary = "获取客户成交周期(按用户)")
+ @PreAuthorize("@ss.hasPermission('crm:statistics-customer:query')")
+ public CommonResult> getCustomerDealCycleByArea(@Valid CrmStatisticsCustomerReqVO reqVO) {
+ return success(customerService.getCustomerDealCycleByArea(reqVO));
+ }
+
+ @GetMapping("/get-customer-deal-cycle-by-product")
+ @Operation(summary = "获取客户成交周期(按用户)")
+ @PreAuthorize("@ss.hasPermission('crm:statistics-customer:query')")
+ public CommonResult> getCustomerDealCycleByProduct(@Valid CrmStatisticsCustomerReqVO reqVO) {
+ return success(customerService.getCustomerDealCycleByProduct(reqVO));
+ }
}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsFunnelController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsFunnelController.java
new file mode 100644
index 000000000..85e0c3d4e
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsFunnelController.java
@@ -0,0 +1,74 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics;
+
+import cn.hutool.extra.spring.SpringUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.business.CrmBusinessController;
+import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import cn.iocoder.yudao.module.crm.service.statistics.CrmStatisticsFunnelService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - CRM 销售漏斗")
+@RestController
+@RequestMapping("/crm/statistics-funnel")
+@Validated
+public class CrmStatisticsFunnelController {
+
+ @Resource
+ private CrmStatisticsFunnelService funnelService;
+
+ @GetMapping("/get-funnel-summary")
+ @Operation(summary = "获取销售漏斗统计数据", description = "用于【销售漏斗】页面的【销售漏斗分析】")
+ @PreAuthorize("@ss.hasPermission('crm:statistics-funnel:query')")
+ public CommonResult getFunnelSummary(@Valid CrmStatisticsFunnelReqVO reqVO) {
+ return success(funnelService.getFunnelSummary(reqVO));
+ }
+
+ @GetMapping("/get-business-summary-by-end-status")
+ @Operation(summary = "获取商机结束状态统计", description = "用于【销售漏斗】页面的【销售漏斗分析】")
+ @PreAuthorize("@ss.hasPermission('crm:statistics-funnel:query')")
+ public CommonResult> getBusinessSummaryByEndStatus(@Valid CrmStatisticsFunnelReqVO reqVO) {
+ return success(funnelService.getBusinessSummaryByEndStatus(reqVO));
+ }
+
+ @GetMapping("/get-business-summary-by-date")
+ @Operation(summary = "获取新增商机分析(按日期)", description = "用于【销售漏斗】页面")
+ @PreAuthorize("@ss.hasPermission('crm:statistics-funnel:query')")
+ public CommonResult> getBusinessSummaryByDate(@Valid CrmStatisticsFunnelReqVO reqVO) {
+ return success(funnelService.getBusinessSummaryByDate(reqVO));
+ }
+
+ @GetMapping("/get-business-inversion-rate-summary-by-date")
+ @Operation(summary = "获取商机转化率分析(按日期)", description = "用于【销售漏斗】页面")
+ @PreAuthorize("@ss.hasPermission('crm:statistics-funnel:query')")
+ public CommonResult> getBusinessInversionRateSummaryByDate(@Valid CrmStatisticsFunnelReqVO reqVO) {
+ return success(funnelService.getBusinessInversionRateSummaryByDate(reqVO));
+ }
+
+ @GetMapping("/get-business-page-by-date")
+ @Operation(summary = "获得商机分页(按日期)", description = "用于【销售漏斗】页面的【新增商机分析】")
+ @PreAuthorize("@ss.hasPermission('crm:business:query')")
+ public CommonResult> getBusinessPageByDate(@Valid CrmStatisticsFunnelReqVO pageVO) {
+ PageResult pageResult = funnelService.getBusinessPageByDate(pageVO);
+ return success(new PageResult<>(buildBusinessDetailList(pageResult.getList()), pageResult.getTotal()));
+ }
+
+ private List buildBusinessDetailList(List list) {
+ return SpringUtil.getBean(CrmBusinessController.class).buildBusinessDetailList(list);
+ }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerDealCycleByAreaRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerDealCycleByAreaRespVO.java
new file mode 100644
index 000000000..369837827
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerDealCycleByAreaRespVO.java
@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - CRM 客户成交周期分析(按区域) VO")
+@Data
+public class CrmStatisticsCustomerDealCycleByAreaRespVO {
+
+ @Schema(description = "省份编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @JsonIgnore
+ private Integer areaId;
+
+ @Schema(description = "省份名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "浙江省")
+ private String areaName;
+
+ @Schema(description = "成交周期", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0")
+ private Double customerDealCycle;
+
+ @Schema(description = "成交客户数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Integer customerDealCount;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerDealCycleByProductRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerDealCycleByProductRespVO.java
new file mode 100644
index 000000000..442c195aa
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerDealCycleByProductRespVO.java
@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - CRM 客户成交周期分析(按产品) VO")
+@Data
+public class CrmStatisticsCustomerDealCycleByProductRespVO {
+
+ @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "演示产品")
+ private String productName;
+
+ @Schema(description = "成交周期", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0")
+ private Double customerDealCycle;
+
+ @Schema(description = "成交客户数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Integer customerDealCount;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticFunnelSummaryRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticFunnelSummaryRespVO.java
new file mode 100644
index 000000000..38d1c118f
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticFunnelSummaryRespVO.java
@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Schema(description = "管理后台 - CRM 销售漏斗 Response VO")
+@NoArgsConstructor
+@AllArgsConstructor
+@Data
+public class CrmStatisticFunnelSummaryRespVO {
+
+ @Schema(description = "客户数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Long customerCount;
+
+ @Schema(description = "商机数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Long businessCount;
+
+ @Schema(description = "赢单数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Long businessWinCount;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsBusinessInversionRateSummaryByDateRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsBusinessInversionRateSummaryByDateRespVO.java
new file mode 100644
index 000000000..b7650a91d
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsBusinessInversionRateSummaryByDateRespVO.java
@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Schema(description = "管理后台 - CRM 商机转化率分析(按日期) VO")
+@Data
+public class CrmStatisticsBusinessInversionRateSummaryByDateRespVO {
+
+ @Schema(description = "时间轴", requiredMode = Schema.RequiredMode.REQUIRED, example = "202401")
+ private String time;
+
+ @Schema(description = "商机数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Long businessCount;
+
+ @Schema(description = "赢单商机数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Long businessWinCount;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsBusinessSummaryByDateRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsBusinessSummaryByDateRespVO.java
new file mode 100644
index 000000000..1f8056c46
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsBusinessSummaryByDateRespVO.java
@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Schema(description = "管理后台 - CRM 新增商机分析(按日期) VO")
+@Data
+public class CrmStatisticsBusinessSummaryByDateRespVO {
+
+ @Schema(description = "时间轴", requiredMode = Schema.RequiredMode.REQUIRED, example = "202401")
+ private String time;
+
+ @Schema(description = "新增商机数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Long businessCreateCount;
+
+ @Schema(description = "新增商机金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private BigDecimal totalPrice;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsBusinessSummaryByEndStatusRespVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsBusinessSummaryByEndStatusRespVO.java
new file mode 100644
index 000000000..023fdb846
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsBusinessSummaryByEndStatusRespVO.java
@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+@Schema(description = "管理后台 - CRM 商机结束状态统计 Response VO")
+@NoArgsConstructor
+@AllArgsConstructor
+@Data
+public class CrmStatisticsBusinessSummaryByEndStatusRespVO {
+
+ @Schema(description = "结束状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Integer endStatus;
+
+ @Schema(description = "商机数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Long businessCount;
+
+ @Schema(description = "商机总金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private BigDecimal totalPrice;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsFunnelReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsFunnelReqVO.java
new file mode 100644
index 000000000..dac340601
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/funnel/CrmStatisticsFunnelReqVO.java
@@ -0,0 +1,47 @@
+package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel;
+
+import cn.iocoder.yudao.framework.common.enums.DateIntervalEnum;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - CRM 销售漏斗 Request VO")
+@Data
+public class CrmStatisticsFunnelReqVO extends PageParam {
+
+ @Schema(description = "部门 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "部门 id 不能为空")
+ private Long deptId;
+
+ /**
+ * 负责人用户 id, 当用户为空, 则计算部门下用户
+ */
+ @Schema(description = "负责人用户 id", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "1")
+ private Long userId;
+
+ /**
+ * userIds 目前不用前端传递,目前是方便后端通过 deptId 读取编号后,设置回来
+ * 后续,可能会支持选择部分用户进行查询
+ */
+ @Schema(description = "负责人用户 id 集合", hidden = true, example = "2")
+ private List userIds;
+
+ @Schema(description = "时间间隔类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @InEnum(value = DateIntervalEnum.class, message = "时间间隔类型,必须是 {value}")
+ private Integer interval;
+
+ @Schema(description = "时间范围", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ @Size(min = 2, max = 2, message = "请选择时间范围")
+ private LocalDateTime[] times;
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/portrait/CrmStatisticsPortraitReqVO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/portrait/CrmStatisticsPortraitReqVO.java
index c284e7457..d16415138 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/portrait/CrmStatisticsPortraitReqVO.java
+++ b/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;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
@@ -31,12 +32,9 @@ public class CrmStatisticsPortraitReqVO {
@Schema(description = "负责人用户 id 集合", hidden = true, example = "2")
private List userIds;
- /**
- * 前端如果选择自定义时间, 那么前端传递起始-终止时间, 如果选择其他时间间隔类型, 则由后台计算起始-终止时间
- * 并作为参数传递给Mapper
- */
@Schema(description = "时间范围", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ @Size(min = 2, max = 2, message = "请选择时间范围")
private LocalDateTime[] times;
}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
index fc5b070f4..ba347bcf6 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
+++ b/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;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessPageReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsFunnelReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
-import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
import cn.iocoder.yudao.module.crm.util.CrmPermissionUtils;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
@@ -59,10 +59,16 @@ public interface CrmBusinessMapper extends BaseMapperX {
return selectCount(CrmBusinessDO::getStatusTypeId, statusTypeId);
}
- default List selectListByCustomerIdOwnerUserId(Long customerId, Long ownerUserId){
+ default List selectListByCustomerIdOwnerUserId(Long customerId, Long ownerUserId) {
return selectList(new LambdaQueryWrapperX()
.eq(CrmBusinessDO::getCustomerId, customerId)
.eq(CrmBusinessDO::getOwnerUserId, ownerUserId));
}
+ default PageResult selectPage(CrmStatisticsFunnelReqVO pageVO) {
+ return selectPage(pageVO, new LambdaQueryWrapperX()
+ .in(CrmBusinessDO::getOwnerUserId, pageVO.getUserIds())
+ .betweenIfPresent(CrmBusinessDO::getCreateTime, pageVO.getTimes()));
+ }
+
}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/statistics/CrmStatisticsCustomerMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/statistics/CrmStatisticsCustomerMapper.java
index 458ef79c3..171d432b0 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/statistics/CrmStatisticsCustomerMapper.java
+++ b/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 {
/**
* 合同总金额(按用户)
+ *
* @return 统计数据@return 统计数据@param reqVO 请求参数
* @return 统计数据
*/
@@ -191,4 +192,20 @@ public interface CrmStatisticsCustomerMapper {
*/
List selectCustomerDealCycleGroupByUser(CrmStatisticsCustomerReqVO reqVO);
+ /**
+ * 客户成交周期(按区域)
+ *
+ * @param reqVO 请求参数
+ * @return 统计数据
+ */
+ List selectCustomerDealCycleGroupByAreaId(CrmStatisticsCustomerReqVO reqVO);
+
+ /**
+ * 客户成交周期(按产品)
+ *
+ * @param reqVO 请求参数
+ * @return 统计数据
+ */
+ List selectCustomerDealCycleGroupByProductId(CrmStatisticsCustomerReqVO reqVO);
+
}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/statistics/CrmStatisticsFunnelMapper.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/statistics/CrmStatisticsFunnelMapper.java
new file mode 100644
index 000000000..d69fa6290
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/statistics/CrmStatisticsFunnelMapper.java
@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.crm.dal.mysql.statistics;
+
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsBusinessInversionRateSummaryByDateRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsBusinessSummaryByDateRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsBusinessSummaryByEndStatusRespVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsFunnelReqVO;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * CRM 销售漏斗 Mapper
+ *
+ * @author HUIHUI
+ */
+@Mapper
+public interface CrmStatisticsFunnelMapper {
+
+ Long selectCustomerCountByDate(CrmStatisticsFunnelReqVO reqVO);
+
+ Long selectBusinessCountByDateAndEndStatus(@Param("reqVO") CrmStatisticsFunnelReqVO reqVO, @Param("status") Integer status);
+
+ List selectBusinessSummaryListGroupByEndStatus(CrmStatisticsFunnelReqVO reqVO);
+
+ List selectBusinessSummaryGroupByDate(CrmStatisticsFunnelReqVO reqVO);
+
+ List selectBusinessInversionRateSummaryByDate(CrmStatisticsFunnelReqVO reqVO);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
index 7bd899b64..abe5a70df 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessService.java
+++ b/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
import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessSaveReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessTransferReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessUpdateStatusReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsFunnelReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessProductDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessStatusDO;
@@ -194,4 +195,12 @@ public interface CrmBusinessService {
*/
List getBusinessListByCustomerIdOwnerUserId(Long customerId, Long ownerUserId);
+ /**
+ * 获得商机分页,目前用于【数据统计】
+ *
+ * @param pageVO 请求
+ * @return 商机分页
+ */
+ PageResult getBusinessPageByDate(CrmStatisticsFunnelReqVO pageVO);
+
}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
index 26f02b2f0..9d80a31ac 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
+++ b/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
import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessTransferReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessUpdateStatusReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.contact.vo.CrmContactBusinessReqVO;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsFunnelReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessProductDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessStatusDO;
@@ -375,4 +376,9 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
return businessMapper.selectListByCustomerIdOwnerUserId(customerId, ownerUserId);
}
+ @Override
+ public PageResult getBusinessPageByDate(CrmStatisticsFunnelReqVO pageVO) {
+ return businessMapper.selectPage(pageVO);
+ }
+
}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java
index 70ebeb44d..9724eeaf2 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java
+++ b/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 {
}
@Override
- @LogRecord(type = CRM_CLUE_TYPE, subType = CRM_CLUE_FOLLOW_UP_SUB_TYPE, bizNo = "{{#id}",
+ @LogRecord(type = CRM_CLUE_TYPE, subType = CRM_CLUE_FOLLOW_UP_SUB_TYPE, bizNo = "{{#id}}",
success = CRM_CLUE_FOLLOW_UP_SUCCESS)
@CrmPermission(bizType = CrmBizTypeEnum.CRM_CLUE, bizId = "#id", level = CrmPermissionLevelEnum.WRITE)
public void updateClueFollowUp(Long id, LocalDateTime contactNextTime, String contactLastContent) {
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
index a99269e08..70b4f9f75 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
+++ b/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 {
/**
* 更新客户的跟进状态
*
- * @param id 编号
+ * @param id 编号
* @param dealStatus 跟进状态
*/
void updateCustomerDealStatus(Long id, Boolean dealStatus);
@@ -47,8 +47,8 @@ public interface CrmCustomerService {
/**
* 更新客户相关的跟进信息
*
- * @param id 编号
- * @param contactNextTime 下次联系时间
+ * @param id 编号
+ * @param contactNextTime 下次联系时间
* @param contactLastContent 最后联系内容
*/
void updateCustomerFollowUp(Long id, LocalDateTime contactNextTime, String contactLastContent);
@@ -99,8 +99,8 @@ public interface CrmCustomerService {
/**
* 获得放入公海提醒的客户分页
*
- * @param pageVO 分页查询
- * @param userId 用户编号
+ * @param pageVO 分页查询
+ * @param userId 用户编号
* @return 客户分页
*/
PageResult getPutPoolRemindCustomerPage(CrmCustomerPageReqVO pageVO, Long userId);
@@ -108,7 +108,7 @@ public interface CrmCustomerService {
/**
* 获得待进入公海的客户数量
*
- * @param userId 用户编号
+ * @param userId 用户编号
* @return 提醒数量
*/
Long getPutPoolRemindCustomerCount(Long userId);
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
index cdb7a32d9..75d3af013 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
+++ b/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;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.crm.enums.LogRecordConstants.*;
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsCustomerService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsCustomerService.java
index 0e00e9c22..70c720b9e 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsCustomerService.java
+++ b/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 {
*/
List getCustomerDealCycleByUser(CrmStatisticsCustomerReqVO reqVO);
+ /**
+ * 客户成交周期(按区域)
+ *
+ * @param reqVO 请求参数
+ * @return 统计数据
+ */
+ List getCustomerDealCycleByArea(CrmStatisticsCustomerReqVO reqVO);
+
+ /**
+ * 客户成交周期(按产品)
+ *
+ * @param reqVO 请求参数
+ * @return 统计数据
+ */
+ List getCustomerDealCycleByProduct(CrmStatisticsCustomerReqVO reqVO);
+
}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsCustomerServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsCustomerServiceImpl.java
index f4787b20f..b7b9a08d1 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsCustomerServiceImpl.java
+++ b/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;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
+import cn.iocoder.yudao.framework.ip.core.Area;
+import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
+import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.*;
import cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsCustomerMapper;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
@@ -19,6 +22,7 @@ import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.function.Function;
import java.util.stream.Stream;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
@@ -290,6 +294,46 @@ public class CrmStatisticsCustomerServiceImpl implements CrmStatisticsCustomerSe
return summaryList;
}
+ @Override
+ public List getCustomerDealCycleByArea(CrmStatisticsCustomerReqVO reqVO) {
+ // 1. 获得用户编号数组
+ List userIds = getUserIds(reqVO);
+ if (CollUtil.isEmpty(userIds)) {
+ return Collections.emptyList();
+ }
+ reqVO.setUserIds(userIds);
+
+ // 2. 获取客户地区统计数据
+ List dealCycleByAreaList = customerMapper.selectCustomerDealCycleGroupByAreaId(reqVO);
+ if (CollUtil.isEmpty(dealCycleByAreaList)) {
+ return Collections.emptyList();
+ }
+
+ // 3. 拼接数据
+ Map areaMap = convertMap(AreaUtils.getByType(AreaTypeEnum.PROVINCE, Function.identity()), Area::getId);
+ return convertList(dealCycleByAreaList, vo -> {
+ if (vo.getAreaId() != null) {
+ Integer parentId = AreaUtils.getParentIdByType(vo.getAreaId(), AreaTypeEnum.PROVINCE);
+ findAndThen(areaMap, parentId, area -> vo.setAreaId(parentId).setAreaName(area.getName()));
+ }
+ return vo;
+ });
+ }
+
+ @Override
+ public List getCustomerDealCycleByProduct(CrmStatisticsCustomerReqVO reqVO) {
+ // 1. 获得用户编号数组
+ List userIds = getUserIds(reqVO);
+ if (CollUtil.isEmpty(userIds)) {
+ return Collections.emptyList();
+ }
+ reqVO.setUserIds(userIds);
+
+ // 2. 获取客户产品统计数据
+ // TODO @dhb52:未读取产品名
+ return customerMapper.selectCustomerDealCycleGroupByProductId(reqVO);
+ }
+
/**
* 拼接用户信息(昵称)
*
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsFunnelService.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsFunnelService.java
new file mode 100644
index 000000000..10458daac
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsFunnelService.java
@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.module.crm.service.statistics;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+
+import java.util.List;
+
+/**
+ * CRM 销售漏斗分析 Service
+ *
+ * @author HUIHUI
+ */
+public interface CrmStatisticsFunnelService {
+
+ /**
+ * 获得销售漏斗数据
+ *
+ * @param reqVO 请求
+ * @return 销售漏斗数据
+ */
+ CrmStatisticFunnelSummaryRespVO getFunnelSummary(CrmStatisticsFunnelReqVO reqVO);
+
+ /**
+ * 获得商机结束状态统计
+ *
+ * @param reqVO 请求
+ * @return 商机结束状态统计
+ */
+ List getBusinessSummaryByEndStatus(CrmStatisticsFunnelReqVO reqVO);
+
+ /**
+ * 获取新增商机分析(按日期)
+ *
+ * @param reqVO 请求
+ * @return 新增商机分析
+ */
+ List getBusinessSummaryByDate(CrmStatisticsFunnelReqVO reqVO);
+
+ /**
+ * 获得商机转化率分析(按日期)
+ *
+ * @param reqVO 请求
+ * @return 商机转化率分析
+ */
+ List getBusinessInversionRateSummaryByDate(CrmStatisticsFunnelReqVO reqVO);
+
+ /**
+ * 获得商机分页(按日期)
+ *
+ * @param pageVO 请求
+ * @return 商机分页
+ */
+ PageResult getBusinessPageByDate(CrmStatisticsFunnelReqVO pageVO);
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsFunnelServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsFunnelServiceImpl.java
new file mode 100644
index 000000000..635f577d1
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsFunnelServiceImpl.java
@@ -0,0 +1,153 @@
+package cn.iocoder.yudao.module.crm.service.statistics;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
+import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.*;
+import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
+import cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsFunnelMapper;
+import cn.iocoder.yudao.module.crm.enums.business.CrmBusinessEndStatusEnum;
+import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+
+/**
+ * CRM 销售漏斗分析 Service 实现类
+ *
+ * @author HUIHUI
+ */
+@Service
+public class CrmStatisticsFunnelServiceImpl implements CrmStatisticsFunnelService {
+
+ @Resource
+ private CrmStatisticsFunnelMapper funnelMapper;
+
+ @Resource
+ private AdminUserApi adminUserApi;
+ @Resource
+ private CrmBusinessService businessService;
+ @Resource
+ private DeptApi deptApi;
+
+ @Override
+ public CrmStatisticFunnelSummaryRespVO getFunnelSummary(CrmStatisticsFunnelReqVO reqVO) {
+ // 1. 获得用户编号数组
+ List userIds = getUserIds(reqVO);
+ if (CollUtil.isEmpty(userIds)) {
+ return null;
+ }
+ reqVO.setUserIds(userIds);
+
+ // 2. 获得漏斗数据
+ Long customerCount = funnelMapper.selectCustomerCountByDate(reqVO);
+ Long businessCount = funnelMapper.selectBusinessCountByDateAndEndStatus(reqVO, null);
+ Long businessWinCount = funnelMapper.selectBusinessCountByDateAndEndStatus(reqVO, CrmBusinessEndStatusEnum.WIN.getStatus());
+ return new CrmStatisticFunnelSummaryRespVO(customerCount, businessCount, businessWinCount);
+ }
+
+ @Override
+ public List getBusinessSummaryByEndStatus(CrmStatisticsFunnelReqVO reqVO) {
+ // 1. 获得用户编号数组
+ reqVO.setUserIds(getUserIds(reqVO));
+ if (CollUtil.isEmpty(reqVO.getUserIds())) {
+ return Collections.emptyList();
+ }
+
+ // 2. 获得统计数据
+ return funnelMapper.selectBusinessSummaryListGroupByEndStatus(reqVO);
+ }
+
+ @Override
+ public List getBusinessSummaryByDate(CrmStatisticsFunnelReqVO reqVO) {
+ // 1. 获得用户编号数组
+ reqVO.setUserIds(getUserIds(reqVO));
+ if (CollUtil.isEmpty(reqVO.getUserIds())) {
+ return Collections.emptyList();
+ }
+
+ // 2. 按天统计,获取分项统计数据
+ List businessSummaryList = funnelMapper.selectBusinessSummaryGroupByDate(reqVO);
+ // 3. 按照日期间隔,合并数据
+ List timeRanges = LocalDateTimeUtils.getDateRangeList(reqVO.getTimes()[0], reqVO.getTimes()[1], reqVO.getInterval());
+ return convertList(timeRanges, times -> {
+ Long businessCreateCount = businessSummaryList.stream()
+ .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], vo.getTime()))
+ .mapToLong(CrmStatisticsBusinessSummaryByDateRespVO::getBusinessCreateCount).sum();
+ BigDecimal businessDealCount = businessSummaryList.stream()
+ .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], vo.getTime()))
+ .map(CrmStatisticsBusinessSummaryByDateRespVO::getTotalPrice)
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+ return new CrmStatisticsBusinessSummaryByDateRespVO()
+ .setTime(LocalDateTimeUtils.formatDateRange(times[0], times[1], reqVO.getInterval()))
+ .setBusinessCreateCount(businessCreateCount).setTotalPrice(businessDealCount);
+ });
+ }
+
+ @Override
+ public List getBusinessInversionRateSummaryByDate(CrmStatisticsFunnelReqVO reqVO) {
+ // 1. 获得用户编号数组
+ reqVO.setUserIds(getUserIds(reqVO));
+ if (CollUtil.isEmpty(reqVO.getUserIds())) {
+ return Collections.emptyList();
+ }
+
+ // 2. 按天统计,获取分项统计数据
+ List businessSummaryList = funnelMapper.selectBusinessInversionRateSummaryByDate(reqVO);
+ // 3. 按照日期间隔,合并数据
+ List timeRanges = LocalDateTimeUtils.getDateRangeList(reqVO.getTimes()[0], reqVO.getTimes()[1], reqVO.getInterval());
+ return convertList(timeRanges, times -> {
+ Long businessCount = businessSummaryList.stream()
+ .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], vo.getTime()))
+ .mapToLong(CrmStatisticsBusinessInversionRateSummaryByDateRespVO::getBusinessCount).sum();
+ Long businessWinCount = businessSummaryList.stream()
+ .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], vo.getTime()))
+ .mapToLong(CrmStatisticsBusinessInversionRateSummaryByDateRespVO::getBusinessWinCount).sum();
+ return new CrmStatisticsBusinessInversionRateSummaryByDateRespVO()
+ .setTime(LocalDateTimeUtils.formatDateRange(times[0], times[1], reqVO.getInterval()))
+ .setBusinessCount(businessCount).setBusinessWinCount(businessWinCount);
+ });
+ }
+
+ @Override
+ public PageResult getBusinessPageByDate(CrmStatisticsFunnelReqVO pageVO) {
+ // 1. 获得用户编号数组
+ pageVO.setUserIds(getUserIds(pageVO));
+ if (CollUtil.isEmpty(pageVO.getUserIds())) {
+ return PageResult.empty();
+ }
+ // 2. 执行查询
+ return businessService.getBusinessPageByDate(pageVO);
+ }
+
+ /**
+ * 获取用户编号数组。如果用户编号为空, 则获得部门下的用户编号数组,包括子部门的所有用户编号
+ *
+ * @param reqVO 请求参数
+ * @return 用户编号数组
+ */
+ private List getUserIds(CrmStatisticsFunnelReqVO reqVO) {
+ // 情况一:选中某个用户
+ if (ObjUtil.isNotNull(reqVO.getUserId())) {
+ return List.of(reqVO.getUserId());
+ }
+ // 情况二:选中某个部门
+ // 2.1 获得部门列表
+ List deptIds = convertList(deptApi.getChildDeptList(reqVO.getDeptId()), DeptRespDTO::getId);
+ deptIds.add(reqVO.getDeptId());
+ // 2.2 获得用户编号
+ return convertList(adminUserApi.getUserListByDeptIds(deptIds), AdminUserRespDTO::getId);
+ }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPerformanceServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPerformanceServiceImpl.java
index 450d691ff..fcd7267a8 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPerformanceServiceImpl.java
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPerformanceServiceImpl.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.crm.service.statistics;
import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO;
@@ -13,8 +14,9 @@ import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
-import java.util.Collections;
-import java.util.List;
+
+import java.math.BigDecimal;
+import java.util.*;
import java.util.function.Function;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@@ -38,7 +40,7 @@ public class CrmStatisticsPerformanceServiceImpl implements CrmStatisticsPerform
@Override
public List getContractCountPerformance(CrmStatisticsPerformanceReqVO performanceReqVO) {
- // TODO @scholar:我们可以换个思路实现,减少数据库的计算量;
+ // TODO @scholar:可以把下面这个注释,你理解后,重新整理下,写到 getPerformance 里;
// 比如说,2024 年的合同数据,是不是 2022-12 到 2024-12-31,每个月的统计呢?
// 理解之后,我们可以数据 group by 年-月,20222-12 到 2024-12-31 的,然后内存在聚合出 CrmStatisticsPerformanceRespVO 这样
// 这样,我们就可以减少数据库的计算量,提升性能;同时 SQL 也会很简单,开发者理解起来也简单哈;
@@ -55,28 +57,99 @@ public class CrmStatisticsPerformanceServiceImpl implements CrmStatisticsPerform
return getPerformance(performanceReqVO, performanceMapper::selectReceivablePricePerformance);
}
+ // TODO @scholar:代码注释,应该有 3 个变量哈;
/**
* 获得员工业绩数据
*
* @param performanceReqVO 参数
- * @param performanceFunction 排行榜方法
- * @return 排行版数据
+ * @param performanceFunction 员工业绩统计方法
+ * @return 员工业绩数据
*/
+ // TODO @scholar:下面一行的变量,超过一行了,阅读不美观;可以考虑每一行一个变量;
private List getPerformance(CrmStatisticsPerformanceReqVO performanceReqVO, Function> performanceFunction) {
+ // TODO @scholar:没使用到的变量,建议删除;
+ List performanceRespVOList;
+
// 1. 获得用户编号数组
final List userIds = getUserIds(performanceReqVO);
if (CollUtil.isEmpty(userIds)) {
return Collections.emptyList();
}
performanceReqVO.setUserIds(userIds);
- // 2. 获得排行数据
+ // TODO @scholar:1. 和 2. 之间,可以考虑换一行;保证每一块逻辑的间隔;
+ // 2. 获得业绩数据
+ // TODO @scholar:复数变量,建议使用 s 或者 list 结果;这里用 performanceList 好列;
List performance = performanceFunction.apply(performanceReqVO);
- if (CollUtil.isEmpty(performance)) {
- return Collections.emptyList();
+
+ // 获取查询的年份
+ // TODO @scholar:逻辑可以简化一下;
+ // TODO 1)把 performance 转换成 map;key 是 time,value 是 count
+ // TODO 2)当前年,遍历 1-12 月份,去 map 拿到 count;接着月份 -1,去 map 拿 count;再年份 -1,拿 count
+ String currentYear = LocalDateTimeUtil.format(performanceReqVO.getTimes()[0],"yyyy");
+
+ // 构造查询当年和前一年,每年12个月的年月组合
+ List allMonths = new ArrayList<>();
+ for (int year = Integer.parseInt(currentYear)-1; year <= Integer.parseInt(currentYear); year++) {
+ for (int month = 1; month <= 12; month++) {
+ allMonths.add(String.format("%d%02d", year, month));
+ }
}
- return performance;
+
+ List computedList = new ArrayList<>();
+ List respVOList = new ArrayList<>();
+
+ // 生成computedList基础数据
+ // 构造完整的2*12个月的数据,如果某月数据缺失,需要补上0,一年12个月不能有缺失
+ for (String month : allMonths) {
+ CrmStatisticsPerformanceRespVO foundData = performance.stream()
+ .filter(data -> data.getTime().equals(month))
+ .findFirst()
+ .orElse(null);
+
+ if (foundData != null) {
+ computedList.add(foundData);
+ } else {
+ CrmStatisticsPerformanceRespVO missingData = new CrmStatisticsPerformanceRespVO();
+ missingData.setTime(month);
+ missingData.setCurrentMonthCount(BigDecimal.ZERO);
+ missingData.setLastMonthCount(BigDecimal.ZERO);
+ missingData.setLastYearCount(BigDecimal.ZERO);
+ computedList.add(missingData);
+ }
+ }
+ //根据查询年份和前一年的数据,计算查询年份的同比环比数据
+ for (CrmStatisticsPerformanceRespVO currentData : computedList) {
+ String currentMonth = currentData.getTime();
+
+ // 根据当年和前一年的月销售数据,计算currentYear的完整数据
+ if (currentMonth.startsWith(currentYear)) {
+ // 计算 LastMonthCount
+ int currentIndex = computedList.indexOf(currentData);
+ if (currentIndex > 0) {
+ CrmStatisticsPerformanceRespVO lastMonthData = computedList.get(currentIndex - 1);
+ currentData.setLastMonthCount(lastMonthData.getCurrentMonthCount());
+ } else {
+ currentData.setLastMonthCount(BigDecimal.ZERO); // 第一个月的 LastMonthCount 设为0
+ }
+
+ // 计算 LastYearCount
+ String lastYearMonth = String.valueOf(Integer.parseInt(currentMonth) - 100);
+ CrmStatisticsPerformanceRespVO lastYearData = computedList.stream()
+ .filter(data -> data.getTime().equals(lastYearMonth))
+ .findFirst()
+ .orElse(null);
+
+ if (lastYearData != null) {
+ currentData.setLastYearCount(lastYearData.getCurrentMonthCount());
+ } else {
+ currentData.setLastYearCount(BigDecimal.ZERO); // 如果去年同月数据不存在,设为0
+ }
+ respVOList.add(currentData);//给前端只需要返回查询当年的数据,不需要前一年数据
+ }
+ }
+ return respVOList;
}
/**
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPortraitServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPortraitServiceImpl.java
index eae012866..83cbb53e7 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsPortraitServiceImpl.java
+++ b/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;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
-import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
/**
* CRM 客户画像 Service 实现类
@@ -55,15 +54,18 @@ public class CrmStatisticsPortraitServiceImpl implements CrmStatisticsPortraitSe
// 3. 拼接数据
List areaList = AreaUtils.getByType(AreaTypeEnum.PROVINCE, area -> area);
- areaList.add(new Area().setId(null).setName("未知")); // TODO @puhui999:是不是 65 find 的逻辑改下;不用 findAndThen,直接从 areaMap 拿;拿到就设置,不拿到就设置 null 和 未知;这样,58 本行可以删除掉完事了;这样代码更简单和一致
Map areaMap = convertMap(areaList, Area::getId);
return convertList(list, item -> {
Integer parentId = AreaUtils.getParentIdByType(item.getAreaId(), AreaTypeEnum.PROVINCE);
- if (parentId == null) { // 找不到,归到未知
- return item.setAreaId(null).setAreaName("未知");
+ if (parentId != null) {
+ Area area = areaMap.get(parentId);
+ if (area != null) {
+ item.setAreaId(parentId).setAreaName(area.getName());
+ return item;
+ }
}
- findAndThen(areaMap, parentId, area -> item.setAreaId(parentId).setAreaName(area.getName()));
- return item;
+ // 找不到,归到未知
+ return item.setAreaId(null).setAreaName("未知");
});
}
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/util/CrmPermissionUtils.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/util/CrmPermissionUtils.java
index 587e9ab56..c42db3e1c 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/util/CrmPermissionUtils.java
+++ b/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 {
}
// 2.2 场景二:我参与的数据
if (CrmSceneTypeEnum.isInvolved(sceneType)) {
- query.ne(ownerUserIdField, userId)
- .in(CrmPermissionDO::getLevel, CrmPermissionLevelEnum.READ.getLevel(), CrmPermissionLevelEnum.WRITE.getLevel());
+ query.innerJoin(CrmPermissionDO.class, on -> on.eq(CrmPermissionDO::getBizType, bizType)
+ .eq(CrmPermissionDO::getBizId, bizId)
+ .in(CrmPermissionDO::getLevel, CrmPermissionLevelEnum.READ.getLevel(), CrmPermissionLevelEnum.WRITE.getLevel()));
+ query.ne(ownerUserIdField, userId);
}
// 2.3 场景三:下属负责的数据
if (CrmSceneTypeEnum.isSubordinate(sceneType)) {
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsCustomerMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsCustomerMapper.xml
index 44c6c4b84..dc10f1221 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsCustomerMapper.xml
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsCustomerMapper.xml
@@ -16,20 +16,20 @@
GROUP BY time
-
@@ -53,13 +53,14 @@
COUNT(DISTINCT customer.id) AS customer_deal_count
FROM crm_customer AS customer
LEFT JOIN crm_contract AS contract ON contract.customer_id = customer.id
- WHERE customer.deleted = 0 AND contract.deleted = 0
+ WHERE customer.deleted = 0
+ AND contract.deleted = 0
AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
AND customer.owner_user_id IN
#{userId}
- AND contract.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
+ AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
GROUP BY customer.owner_user_id
@@ -221,4 +222,45 @@
GROUP BY customer.owner_user_id
+
+
+
+
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsFunnelMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsFunnelMapper.xml
new file mode 100644
index 000000000..a07406259
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsFunnelMapper.xml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsPerformanceMapper.xml b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsPerformanceMapper.xml
index 10b952bac..79ff45471 100644
--- a/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsPerformanceMapper.xml
+++ b/yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsPerformanceMapper.xml
@@ -5,75 +5,28 @@
+
+
+
-
diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/comment/ProductCommentServiceImpl.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/comment/ProductCommentServiceImpl.java
index a089e2673..83c8e93a1 100644
--- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/comment/ProductCommentServiceImpl.java
+++ b/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 {
// 更新可见状态
productCommentMapper.updateById(new ProductCommentDO().setId(updateReqVO.getId())
- .setVisible(true));
+ .setVisible(updateReqVO.getVisible()));
}
@Override
diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/delivery/DeliveryExpressTemplateServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/delivery/DeliveryExpressTemplateServiceImpl.java
index c939e8531..7aed42803 100644
--- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/delivery/DeliveryExpressTemplateServiceImpl.java
+++ b/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
List oldList = expressTemplateFreeMapper.selectListByTemplateId(templateId);
List newList = INSTANCE.convertTemplateFreeList(templateId, frees);
List> diffList = CollectionUtils.diffList(oldList, newList,
- (oldVal, newVal) -> ObjectUtil.equal(oldVal.getId(), newVal.getTemplateId()));
+ (oldVal, newVal) -> ObjectUtil.equal(oldVal.getId(), newVal.getId()));
// 第二步,批量添加、修改、删除
if (CollUtil.isNotEmpty(diffList.get(0))) {
diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java
index 036d73d04..58c36c312 100644
--- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java
+++ b/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 {
@Resource
private PayWalletMapper walletMapper;
+
@Resource
+ @Lazy // 延迟加载,避免循环依赖
private PayWalletTransactionService walletTransactionService;
@Resource
- @Lazy
+ @Lazy // 延迟加载,避免循环依赖
private PayOrderService orderService;
@Resource
- @Lazy
+ @Lazy // 延迟加载,避免循环依赖
private PayRefundService refundService;
@Override
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptRespVO.java
index 777cb75de..83e6ed085 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptRespVO.java
+++ b/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 {
@Schema(description = "父部门 ID", example = "1024")
private Long parentId;
- @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Integer sort;
@Schema(description = "负责人的用户编号", example = "2048")
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptSaveReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptSaveReqVO.java
index 7b395dac8..4d25b7cfb 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/dept/DeptSaveReqVO.java
+++ b/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 {
@Schema(description = "父部门 ID", example = "1024")
private Long parentId;
- @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "显示顺序不能为空")
private Integer sort;
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostRespVO.java
index a037a132a..dde6f9509 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostRespVO.java
+++ b/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 {
@ExcelProperty("岗位编码")
private String code;
- @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@ExcelProperty("岗位排序")
private Integer sort;
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostSaveReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostSaveReqVO.java
index 91f69e545..5ac2be372 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostSaveReqVO.java
+++ b/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 {
@Size(max = 64, message = "岗位编码长度不能超过64个字符")
private String code;
- @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "显示顺序不能为空")
private Integer sort;
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataRespVO.java
index e0b940a58..8857a7059 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataRespVO.java
+++ b/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 {
@ExcelProperty("字典编码")
private Long id;
- @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@ExcelProperty("字典排序")
private Integer sort;
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataSaveReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataSaveReqVO.java
index 8e4ee4f27..ca71572ac 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataSaveReqVO.java
+++ b/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 {
@Schema(description = "字典数据编号", example = "1024")
private Long id;
- @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "显示顺序不能为空")
private Integer sort;
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuRespVO.java
index 39e752942..8dff186e0 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuRespVO.java
+++ b/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 {
@NotNull(message = "菜单类型不能为空")
private Integer type;
- @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "显示顺序不能为空")
private Integer sort;
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuSaveVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuSaveVO.java
index beb4b3869..9f96f19ad 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/menu/MenuSaveVO.java
+++ b/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 {
@NotNull(message = "菜单类型不能为空")
private Integer type;
- @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "显示顺序不能为空")
private Integer sort;
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java
index f249406fa..e7b48c8bc 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java
+++ b/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 {
@ExcelProperty("角色标志")
private String code;
- @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@ExcelProperty("角色排序")
private Integer sort;
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleSaveReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleSaveReqVO.java
index dc97bc276..ee5951fc0 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleSaveReqVO.java
+++ b/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 {
@DiffLogField(name = "角色标志")
private String code;
- @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ @Schema(description = "显示顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "显示顺序不能为空")
@DiffLogField(name = "显示顺序")
private Integer sort;
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java
index 694a64d2f..beee296d1 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java
+++ b/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;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuSaveVO;
import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuListReqVO;
+import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuSaveVO;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
import cn.iocoder.yudao.module.system.dal.mysql.permission.MenuMapper;
import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants;
import cn.iocoder.yudao.module.system.enums.permission.MenuTypeEnum;
import cn.iocoder.yudao.module.system.service.tenant.TenantService;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
@@ -17,7 +19,6 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
-import jakarta.annotation.Resource;
import java.util.Collection;
import java.util.List;
@@ -130,6 +131,10 @@ public class MenuServiceImpl implements MenuService {
@Override
public List getMenuList(Collection ids) {
+ // 当 ids 为空时,返回一个空的实例对象
+ if (CollUtil.isEmpty(ids)) {
+ return Lists.newArrayList();
+ }
return menuMapper.selectBatchIds(ids);
}
diff --git a/yudao-server/src/main/resources/application-dev.yaml b/yudao-server/src/main/resources/application-dev.yaml
index c10cdcb45..ead65d51e 100644
--- a/yudao-server/src/main/resources/application-dev.yaml
+++ b/yudao-server/src/main/resources/application-dev.yaml
@@ -188,6 +188,16 @@ justauth:
client-secret: 1wTb7hYxnpT2TUbIeHGXGo7T0odav1ic10mLdyyATOw
agent-id: 1000004
ignore-check-redirect-uri: true
+ # noinspection SpringBootApplicationYaml
+ WECHAT_MINI_APP: # 微信小程序
+ client-id: ${wx.miniapp.appid}
+ client-secret: ${wx.miniapp.secret}
+ ignore-check-redirect-uri: true
+ ignore-check-state: true # 微信小程序,不会使用到 state,所以不进行校验
+ WECHAT_MP: # 微信公众号
+ client-id: ${wx.mp.app-id}
+ client-secret: ${wx.mp.secret}
+ ignore-check-redirect-uri: true
cache:
type: REDIS
prefix: 'social_auth_state:' # 缓存前缀,目前只对 Redis 缓存生效,默认 JUSTAUTH::STATE::
diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml
index 7f348da84..ea1a7e809 100644
--- a/yudao-server/src/main/resources/application-local.yaml
+++ b/yudao-server/src/main/resources/application-local.yaml
@@ -242,6 +242,7 @@ justauth:
client-secret: 1wTb7hYxnpT2TUbIeHGXGo7T0odav1ic10mLdyyATOw
agent-id: 1000004
ignore-check-redirect-uri: true
+ # noinspection SpringBootApplicationYaml
WECHAT_MINI_APP: # 微信小程序
client-id: ${wx.miniapp.appid}
client-secret: ${wx.miniapp.secret}