Commit decdfb2aa717887ba18f83277e9ba254dffea534

Authored by 芋道源码
Committed by Gitee
2 parents c4ce9068 d9407ab4

!937 feat: 客户成交周期分析(按区域、按产品)

Merge pull request !937 from dhb52/develop
Showing 23 changed files with 862 additions and 27 deletions
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java
... ... @@ -75,4 +75,8 @@ public interface ErrorCodeConstants {
75 75 // ========== BPM 流程表达式 1-009-014-000 ==========
76 76 ErrorCode PROCESS_EXPRESSION_NOT_EXISTS = new ErrorCode(1_009_014_000, "流程表达式不存在");
77 77  
  78 + // ========== BPM 仿钉钉流程设计器 1-009-015-000 ==========
  79 + // TODO @芋艿:这个错误码,需要关注下
  80 + ErrorCode CONVERT_TO_SIMPLE_MODEL_NOT_SUPPORT = new ErrorCode(1_009_015_000, "该流程模型不支持仿钉钉设计流程");
  81 +
78 82 }
... ...
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeType.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.enums.definition;
  2 +
  3 +import cn.hutool.core.util.ArrayUtil;
  4 +import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
  5 +import lombok.AllArgsConstructor;
  6 +import lombok.Getter;
  7 +
  8 +import java.util.Arrays;
  9 +import java.util.Objects;
  10 +
  11 +/**
  12 + * 仿钉钉的流程器设计器的模型节点类型
  13 + *
  14 + * @author jason
  15 + */
  16 +@Getter
  17 +@AllArgsConstructor
  18 +public enum BpmSimpleModelNodeType implements IntArrayValuable {
  19 +
  20 + // TODO @jaosn:-1、0、1、4、-2 是前端已经定义好的么?感觉未来可以考虑搞成和 BPMN 尽量一致的单词哈;类似 usertask 用户审批;
  21 + START_EVENT_NODE(0, "开始节点"),
  22 + APPROVE_USER_NODE (1, "审批人节点"),
  23 + // 抄送人节点、对应 BPMN 的 ScriptTask. 使用ScriptTask 原因。好像 ServiceTask 自定义属性不能写入 XML
  24 + SCRIPT_TASK_NODE(2, "抄送人节点"),
  25 + EXCLUSIVE_GATEWAY_NODE(4, "排他网关"),
  26 + END_EVENT_NODE(-2, "结束节点");
  27 +
  28 + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmSimpleModelNodeType::getType).toArray();
  29 +
  30 + private final Integer type;
  31 + private final String name;
  32 +
  33 + public static boolean isGatewayNode(Integer type) {
  34 + // TODO 后续增加并行网关的支持
  35 + return Objects.equals(EXCLUSIVE_GATEWAY_NODE.getType(), type);
  36 + }
  37 +
  38 + public static BpmSimpleModelNodeType valueOf(Integer type) {
  39 + return ArrayUtil.firstMatch(nodeType -> nodeType.getType().equals(type), values());
  40 + }
  41 +
  42 + @Override
  43 + public int[] array() {
  44 + return ARRAYS;
  45 + }
  46 +
  47 +}
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmSimpleModelController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.controller.admin.definition;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.simple.BpmSimpleModelNodeVO;
  5 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.simple.BpmSimpleModelSaveReqVO;
  6 +import cn.iocoder.yudao.module.bpm.service.definition.BpmSimpleModelService;
  7 +import io.swagger.v3.oas.annotations.Operation;
  8 +import io.swagger.v3.oas.annotations.Parameter;
  9 +import io.swagger.v3.oas.annotations.tags.Tag;
  10 +import jakarta.annotation.Resource;
  11 +import jakarta.validation.Valid;
  12 +import org.springframework.security.access.prepost.PreAuthorize;
  13 +import org.springframework.web.bind.annotation.*;
  14 +
  15 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  16 +
  17 +// TODO @芋艿:后续考虑下,怎么放这个 Controller
  18 +@Tag(name = "管理后台 - BPM 仿钉钉流程设计器")
  19 +@RestController
  20 +@RequestMapping("/bpm/simple")
  21 +public class BpmSimpleModelController {
  22 + @Resource
  23 + private BpmSimpleModelService bpmSimpleModelService;
  24 +
  25 + @PostMapping("/save")
  26 + @Operation(summary = "保存仿钉钉流程设计模型")
  27 + @PreAuthorize("@ss.hasPermission('bpm:model:update')")
  28 + public CommonResult<Boolean> saveSimpleModel(@Valid @RequestBody BpmSimpleModelSaveReqVO reqVO) {
  29 + return success(bpmSimpleModelService.saveSimpleModel(reqVO));
  30 + }
  31 +
  32 + @GetMapping("/get")
  33 + @Operation(summary = "获得仿钉钉流程设计模型")
  34 + @Parameter(name = "modelId", description = "流程模型编号", required = true, example = "a2c5eee0-eb6c-11ee-abf4-0c37967c420a")
  35 + public CommonResult<BpmSimpleModelNodeVO> getSimpleModel(@RequestParam("modelId") String modelId){
  36 + return success(bpmSimpleModelService.getSimpleModel(modelId));
  37 + }
  38 +
  39 +}
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/simple/BpmSimpleModelNodeVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.simple;
  2 +
  3 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  4 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeType;
  5 +import com.fasterxml.jackson.annotation.JsonInclude;
  6 +import io.swagger.v3.oas.annotations.media.Schema;
  7 +import jakarta.validation.constraints.NotEmpty;
  8 +import jakarta.validation.constraints.NotNull;
  9 +import lombok.Data;
  10 +
  11 +import java.util.List;
  12 +import java.util.Map;
  13 +
  14 +@Schema(description = "管理后台 - 仿钉钉流程设计模型节点 VO")
  15 +@Data
  16 +@JsonInclude(JsonInclude.Include.NON_NULL)
  17 +public class BpmSimpleModelNodeVO {
  18 +
  19 + @Schema(description = "模型节点编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "StartEvent_1")
  20 + @NotEmpty(message = "模型节点编号不能为空")
  21 + private String id;
  22 +
  23 + @Schema(description = "模型节点类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  24 + @NotNull(message = "模型节点类型不能为空")
  25 + @InEnum(BpmSimpleModelNodeType.class)
  26 + private Integer type;
  27 +
  28 + @Schema(description = "模型节点名称", example = "领导审批")
  29 + private String name;
  30 +
  31 + @Schema(description = "孩子节点")
  32 + private BpmSimpleModelNodeVO childNode;
  33 +
  34 + @Schema(description = "网关节点的条件节点")
  35 + private List<BpmSimpleModelNodeVO> conditionNodes;
  36 +
  37 + @Schema(description = "节点的属性")
  38 + private Map<String, Object> attributes;
  39 +
  40 +}
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/simple/BpmSimpleModelSaveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.simple;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.Valid;
  5 +import jakarta.validation.constraints.NotEmpty;
  6 +import jakarta.validation.constraints.NotNull;
  7 +import lombok.Data;
  8 +
  9 +// TODO @芋艿:或许挪到 model 里的 simple 包
  10 +@Schema(description = "管理后台 - 仿钉钉流程设计模型的新增/修改 Request VO")
  11 +@Data
  12 +public class BpmSimpleModelSaveReqVO {
  13 +
  14 + @Schema(description = "流程模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  15 + @NotEmpty(message = "流程模型编号不能为空")
  16 + private String modelId; // 对应 Flowable act_re_model 表 ID_ 字段
  17 +
  18 + @Schema(description = "仿钉钉流程设计模型对象", requiredMode = Schema.RequiredMode.REQUIRED)
  19 + @NotNull(message = "仿钉钉流程设计模型对象不能为空")
  20 + @Valid
  21 + private BpmSimpleModelNodeVO simpleModelBody;
  22 +
  23 +}
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnModelConstants.java
1 1 package cn.iocoder.yudao.module.bpm.framework.flowable.core.enums;
2 2  
  3 +import com.google.common.collect.ImmutableSet;
  4 +import org.flowable.bpmn.model.*;
  5 +
  6 +import java.util.Set;
  7 +
3 8 /**
4 9 * BPMN XML 常量信息
5 10 *
... ... @@ -23,4 +28,15 @@ public interface BpmnModelConstants {
23 28 */
24 29 String USER_TASK_CANDIDATE_PARAM = "candidateParam";
25 30  
  31 + // TODO @芋艿:这里后面得关注下;
  32 + /**
  33 + * BPMN End Event 节点 Id, 用于后端生成 End Event 节点
  34 + */
  35 + String END_EVENT_ID = "EndEvent_1";
  36 +
  37 + /**
  38 + * 支持转仿钉钉设计模型的 Bpmn 节点
  39 + */
  40 + Set<Class<? extends FlowNode>> SUPPORT_CONVERT_SIMPLE_FlOW_NODES = ImmutableSet.of(UserTask.class, EndEvent.class);
  41 +
26 42 }
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java
1 1 package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.lang.Assert;
  5 +import cn.hutool.core.map.MapUtil;
4 6 import cn.hutool.core.util.ArrayUtil;
  7 +import cn.hutool.core.util.StrUtil;
5 8 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
  9 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.simple.BpmSimpleModelNodeVO;
  10 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeType;
6 11 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
  12 +import org.flowable.bpmn.BpmnAutoLayout;
7 13 import org.flowable.bpmn.converter.BpmnXMLConverter;
8 14 import org.flowable.bpmn.model.Process;
9 15 import org.flowable.bpmn.model.*;
  16 +import org.flowable.common.engine.impl.scripting.ScriptingEngines;
10 17 import org.flowable.common.engine.impl.util.io.BytesStreamSource;
11 18  
12   -import java.util.*;
  19 +import java.util.ArrayList;
  20 +import java.util.HashSet;
  21 +import java.util.List;
  22 +import java.util.Set;
  23 +
  24 +import static org.flowable.bpmn.constants.BpmnXMLConstants.*;
13 25  
14 26 /**
15 27 * 流程模型转操作工具类
16 28 */
17 29 public class BpmnModelUtils {
18 30  
  31 + public static final String BPMN_SIMPLE_COPY_EXECUTION_SCRIPT = "#{bpmSimpleNodeService.copy(execution)}";
  32 +
19 33 public static Integer parseCandidateStrategy(FlowElement userTask) {
20 34 return NumberUtils.parseInt(userTask.getAttributeValue(
21 35 BpmnModelConstants.NAMESPACE, BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY));
... ... @@ -326,4 +340,196 @@ public class BpmnModelUtils {
326 340 return userTaskList;
327 341 }
328 342  
  343 + // ========== TODO 芋艿:这里得捉摸下; ==========
  344 +
  345 + /**
  346 + * 仿钉钉流程设计模型数据结构(json) 转换成 Bpmn Model (待完善)
  347 + *
  348 + * @param processId 流程标识
  349 + * @param processName 流程名称
  350 + * @param simpleModelNode 仿钉钉流程设计模型数据结构
  351 + * @return Bpmn Model
  352 + */
  353 + public static BpmnModel convertSimpleModelToBpmnModel(String processId, String processName, BpmSimpleModelNodeVO simpleModelNode) {
  354 + BpmnModel bpmnModel = new BpmnModel();
  355 + Process mainProcess = new Process();
  356 + mainProcess.setId(processId);
  357 + mainProcess.setName(processName);
  358 + mainProcess.setExecutable(Boolean.TRUE);
  359 + bpmnModel.addProcess(mainProcess);
  360 + // 前端模型数据结构。 有 start event 节点. 没有 end event 节点。
  361 + // 添加 FlowNode
  362 + addBpmnFlowNode(mainProcess, simpleModelNode);
  363 + // 单独添加 end event 节点
  364 + addBpmnEndEventNode(mainProcess);
  365 + // 添加节点之间的连线 Sequence Flow
  366 + addBpmnSequenceFlow(mainProcess, simpleModelNode, BpmnModelConstants.END_EVENT_ID);
  367 + // 自动布局
  368 + new BpmnAutoLayout(bpmnModel).execute();
  369 + return bpmnModel;
  370 + }
  371 +
  372 + private static void addBpmnSequenceFlow(Process mainProcess, BpmSimpleModelNodeVO node, String endId) {
  373 + // 节点为 null 退出
  374 + if (node == null || node.getId() == null) {
  375 + return;
  376 + }
  377 + BpmSimpleModelNodeVO childNode = node.getChildNode();
  378 + // 如果不是网关节点、且后续节点为 null. 添加与结束节点的连线
  379 + if (!BpmSimpleModelNodeType.isGatewayNode(node.getType()) && (childNode == null || childNode.getId() == null)) {
  380 + addBpmnSequenceFlowElement(mainProcess, node.getId(), endId, null, null);
  381 + return;
  382 + }
  383 + BpmSimpleModelNodeType nodeType = BpmSimpleModelNodeType.valueOf(node.getType());
  384 + Assert.notNull(nodeType, "模型节点类型不支持");
  385 + switch (nodeType) {
  386 + case START_EVENT_NODE:
  387 + case APPROVE_USER_NODE:
  388 + case SCRIPT_TASK_NODE: {
  389 + addBpmnSequenceFlowElement(mainProcess, node.getId(), childNode.getId(), null, null);
  390 + // 递归调用后续节点
  391 + addBpmnSequenceFlow(mainProcess, childNode, endId);
  392 + break;
  393 + }
  394 + case EXCLUSIVE_GATEWAY_NODE: {
  395 + String gateWayEndId = (childNode == null || childNode.getId() == null) ? BpmnModelConstants.END_EVENT_ID : childNode.getId();
  396 + List<BpmSimpleModelNodeVO> conditionNodes = node.getConditionNodes();
  397 + Assert.notEmpty(conditionNodes, "网关节点的条件节点不能为空");
  398 + for (int i = 0; i < conditionNodes.size(); i++) {
  399 + BpmSimpleModelNodeVO item = conditionNodes.get(i);
  400 + BpmSimpleModelNodeVO nextNodeOnCondition = item.getChildNode();
  401 + if (nextNodeOnCondition != null && nextNodeOnCondition.getId() != null) {
  402 + addBpmnSequenceFlowElement(mainProcess, node.getId(), nextNodeOnCondition.getId(),
  403 + String.format("%s_SequenceFlow_%d", node.getId(), i + 1), null);
  404 + addBpmnSequenceFlow(mainProcess, nextNodeOnCondition, gateWayEndId);
  405 + } else {
  406 + addBpmnSequenceFlowElement(mainProcess, node.getId(), gateWayEndId,
  407 + String.format("%s_SequenceFlow_%d", node.getId(), i + 1), null);
  408 + }
  409 + }
  410 + // 递归调用后续节点
  411 + addBpmnSequenceFlow(mainProcess, childNode, endId);
  412 + break;
  413 + }
  414 + default: {
  415 + // TODO 其它节点类型的实现
  416 + }
  417 + }
  418 +
  419 + }
  420 +
  421 + private static void addBpmnSequenceFlowElement(Process mainProcess, String sourceId, String targetId, String seqFlowId, String conditionExpression) {
  422 + SequenceFlow sequenceFlow = new SequenceFlow(sourceId, targetId);
  423 + if (StrUtil.isNotEmpty(conditionExpression)) {
  424 + sequenceFlow.setConditionExpression(conditionExpression);
  425 + }
  426 + if (StrUtil.isNotEmpty(seqFlowId)) {
  427 + sequenceFlow.setId(seqFlowId);
  428 + }
  429 + mainProcess.addFlowElement(sequenceFlow);
  430 + }
  431 +
  432 + private static void addBpmnFlowNode(Process mainProcess, BpmSimpleModelNodeVO simpleModelNode) {
  433 + // 节点为 null 退出
  434 + if (simpleModelNode == null || simpleModelNode.getId() == null) {
  435 + return;
  436 + }
  437 + BpmSimpleModelNodeType nodeType = BpmSimpleModelNodeType.valueOf(simpleModelNode.getType());
  438 + Assert.notNull(nodeType, "模型节点类型不支持");
  439 + switch (nodeType) {
  440 + case START_EVENT_NODE:
  441 + addBpmnStartEventNode(mainProcess, simpleModelNode);
  442 + break;
  443 + case APPROVE_USER_NODE:
  444 + addBpmnUserTaskNode(mainProcess, simpleModelNode);
  445 + break;
  446 + case SCRIPT_TASK_NODE:
  447 + addBpmnScriptTaSskNode(mainProcess, simpleModelNode);
  448 + break;
  449 + case EXCLUSIVE_GATEWAY_NODE:
  450 + addBpmnExclusiveGatewayNode(mainProcess, simpleModelNode);
  451 + break;
  452 + default: {
  453 + // TODO 其它节点类型的实现
  454 + }
  455 + }
  456 +
  457 + // 如果不是网关类型的接口, 并且chileNode为空退出
  458 + if (!BpmSimpleModelNodeType.isGatewayNode(simpleModelNode.getType()) && simpleModelNode.getChildNode() == null) {
  459 + return;
  460 + }
  461 +
  462 + // 如果是网关类型接口. 递归添加条件节点
  463 + if (BpmSimpleModelNodeType.isGatewayNode(simpleModelNode.getType()) && ArrayUtil.isNotEmpty(simpleModelNode.getConditionNodes())) {
  464 + for (BpmSimpleModelNodeVO node : simpleModelNode.getConditionNodes()) {
  465 + addBpmnFlowNode(mainProcess, node.getChildNode());
  466 + }
  467 + }
  468 +
  469 + // chileNode不为空,递归添加子节点
  470 + if (simpleModelNode.getChildNode() != null) {
  471 + addBpmnFlowNode(mainProcess, simpleModelNode.getChildNode());
  472 + }
  473 + }
  474 +
  475 + private static void addBpmnScriptTaSskNode(Process mainProcess, BpmSimpleModelNodeVO node) {
  476 + ScriptTask scriptTask = new ScriptTask();
  477 + scriptTask.setId(node.getId());
  478 + scriptTask.setName(node.getName());
  479 + scriptTask.setScriptFormat(ScriptingEngines.DEFAULT_SCRIPTING_LANGUAGE);
  480 + scriptTask.setScript(BPMN_SIMPLE_COPY_EXECUTION_SCRIPT);
  481 + // 添加自定义属性
  482 + addExtensionAttributes(node, scriptTask);
  483 + mainProcess.addFlowElement(scriptTask);
  484 + }
  485 +
  486 + private static void addExtensionAttributes(BpmSimpleModelNodeVO node, FlowElement flowElement) {
  487 + Integer candidateStrategy = MapUtil.getInt(node.getAttributes(), BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY);
  488 + addExtensionAttributes(flowElement, BpmnModelConstants.NAMESPACE, BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY,
  489 + candidateStrategy == null ? null : String.valueOf(candidateStrategy));
  490 + addExtensionAttributes(flowElement, BpmnModelConstants.NAMESPACE, BpmnModelConstants.USER_TASK_CANDIDATE_PARAM,
  491 + MapUtil.getStr(node.getAttributes(), BpmnModelConstants.USER_TASK_CANDIDATE_PARAM));
  492 + }
  493 +
  494 + private static void addBpmnExclusiveGatewayNode(Process mainProcess, BpmSimpleModelNodeVO node) {
  495 + Assert.notEmpty(node.getConditionNodes(), "网关节点的条件节点不能为空");
  496 + ExclusiveGateway exclusiveGateway = new ExclusiveGateway();
  497 + exclusiveGateway.setId(node.getId());
  498 + // 条件节点的最后一个条件为 网关的 default sequence flow
  499 + exclusiveGateway.setDefaultFlow(String.format("%s_SequenceFlow_%d", node.getId(), node.getConditionNodes().size()));
  500 + mainProcess.addFlowElement(exclusiveGateway);
  501 + }
  502 +
  503 + private static void addBpmnEndEventNode(Process mainProcess) {
  504 + EndEvent endEvent = new EndEvent();
  505 + endEvent.setId(BpmnModelConstants.END_EVENT_ID);
  506 + endEvent.setName("结束");
  507 + mainProcess.addFlowElement(endEvent);
  508 + }
  509 +
  510 + private static void addBpmnUserTaskNode(Process mainProcess, BpmSimpleModelNodeVO node) {
  511 + UserTask userTask = new UserTask();
  512 + userTask.setId(node.getId());
  513 + userTask.setName(node.getName());
  514 + addExtensionAttributes(node, userTask);
  515 + mainProcess.addFlowElement(userTask);
  516 + }
  517 +
  518 + private static void addExtensionAttributes(FlowElement element, String namespace, String name, String value) {
  519 + if (value == null) {
  520 + return;
  521 + }
  522 + ExtensionAttribute extensionAttribute = new ExtensionAttribute(name, value);
  523 + extensionAttribute.setNamespace(namespace);
  524 + extensionAttribute.setNamespacePrefix(FLOWABLE_EXTENSIONS_PREFIX);
  525 + element.addAttribute(extensionAttribute);
  526 + }
  527 +
  528 + private static void addBpmnStartEventNode(Process mainProcess, BpmSimpleModelNodeVO node) {
  529 + StartEvent startEvent = new StartEvent();
  530 + startEvent.setId(node.getId());
  531 + startEvent.setName(node.getName());
  532 + mainProcess.addFlowElement(startEvent);
  533 + }
  534 +
329 535 }
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java
... ... @@ -46,6 +46,30 @@ public interface BpmModelService {
46 46 */
47 47 byte[] getModelBpmnXML(String id);
48 48  
  49 +
  50 + /**
  51 + * 保存流程模型的 BPMN XML
  52 + *
  53 + * @param id 编号
  54 + * @param xmlBytes BPMN XML bytes
  55 + */
  56 + // TODO @芋艿:可能要关注下;
  57 + void saveModelBpmnXml(String id, byte[] xmlBytes);
  58 +
  59 + /**
  60 + * 获得仿钉钉快搭模型的 JSON 数据
  61 + * @param id 编号
  62 + * @return JSON bytes
  63 + */
  64 + byte[] getModelSimpleJson(String id);
  65 +
  66 + /**
  67 + * 保存仿钉钉快搭模型的 JSON 数据
  68 + * @param id 编号
  69 + * @param jsonBytes JSON bytes
  70 + */
  71 + void saveModelSimpleJson(String id, byte[] jsonBytes);
  72 +
49 73 /**
50 74 * 修改流程模型
51 75 *
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java
1 1 package cn.iocoder.yudao.module.bpm.service.definition;
2 2  
  3 +import cn.hutool.core.util.ArrayUtil;
3 4 import cn.hutool.core.util.StrUtil;
4 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 6 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
... ... @@ -103,7 +104,7 @@ public class BpmModelServiceImpl implements BpmModelService {
103 104 // 保存流程定义
104 105 repositoryService.saveModel(model);
105 106 // 保存 BPMN XML
106   - saveModelBpmnXml(model, bpmnXml);
  107 + saveModelBpmnXml(model.getId(), StrUtil.utf8Bytes(bpmnXml));
107 108 return model.getId();
108 109 }
109 110  
... ... @@ -121,7 +122,7 @@ public class BpmModelServiceImpl implements BpmModelService {
121 122 // 更新模型
122 123 repositoryService.saveModel(model);
123 124 // 更新 BPMN XML
124   - saveModelBpmnXml(model, updateReqVO.getBpmnXml());
  125 + saveModelBpmnXml(model.getId(), StrUtil.utf8Bytes(updateReqVO.getBpmnXml()));
125 126 }
126 127  
127 128 @Override
... ... @@ -236,11 +237,25 @@ public class BpmModelServiceImpl implements BpmModelService {
236 237 }
237 238 }
238 239  
239   - private void saveModelBpmnXml(Model model, String bpmnXml) {
240   - if (StrUtil.isEmpty(bpmnXml)) {
  240 + @Override
  241 + public void saveModelBpmnXml(String id, byte[] xmlBytes) {
  242 + if (ArrayUtil.isEmpty(xmlBytes)) {
  243 + return;
  244 + }
  245 + repositoryService.addModelEditorSource(id, xmlBytes);
  246 + }
  247 +
  248 + @Override
  249 + public byte[] getModelSimpleJson(String id) {
  250 + return repositoryService.getModelEditorSourceExtra(id);
  251 + }
  252 +
  253 + @Override
  254 + public void saveModelSimpleJson(String id, byte[] jsonBytes) {
  255 + if (ArrayUtil.isEmpty(jsonBytes)) {
241 256 return;
242 257 }
243   - repositoryService.addModelEditorSource(model.getId(), StrUtil.utf8Bytes(bpmnXml));
  258 + repositoryService.addModelEditorSourceExtra(id, jsonBytes);
244 259 }
245 260  
246 261 /**
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmSimpleModelService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.service.definition;
  2 +
  3 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.simple.BpmSimpleModelNodeVO;
  4 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.simple.BpmSimpleModelSaveReqVO;
  5 +import jakarta.validation.Valid;
  6 +
  7 +/**
  8 + * 仿钉钉流程设计 Service 接口
  9 + *
  10 + * @author jason
  11 + */
  12 +public interface BpmSimpleModelService {
  13 +
  14 + /**
  15 + * 保存仿钉钉流程设计模型
  16 + *
  17 + * @param reqVO 请求信息
  18 + */
  19 + Boolean saveSimpleModel(@Valid BpmSimpleModelSaveReqVO reqVO);
  20 +
  21 + /**
  22 + * 获取仿钉钉流程设计模型结构
  23 + *
  24 + * @param modelId 流程模型编号
  25 + * @return 仿钉钉流程设计模型结构
  26 + */
  27 + BpmSimpleModelNodeVO getSimpleModel(String modelId);
  28 +
  29 +}
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmSimpleModelServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.service.definition;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.lang.Assert;
  5 +import cn.hutool.core.map.MapUtil;
  6 +import cn.hutool.core.util.StrUtil;
  7 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  8 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.simple.BpmSimpleModelNodeVO;
  9 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.simple.BpmSimpleModelSaveReqVO;
  10 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeType;
  11 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
  12 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
  13 +import jakarta.annotation.Resource;
  14 +import org.flowable.bpmn.model.*;
  15 +import org.flowable.engine.repository.Model;
  16 +import org.springframework.stereotype.Service;
  17 +import org.springframework.validation.annotation.Validated;
  18 +
  19 +import java.util.List;
  20 +import java.util.Map;
  21 +
  22 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  23 +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.CONVERT_TO_SIMPLE_MODEL_NOT_SUPPORT;
  24 +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.MODEL_NOT_EXISTS;
  25 +import static cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeType.START_EVENT_NODE;
  26 +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.USER_TASK_CANDIDATE_PARAM;
  27 +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY;
  28 +
  29 +/**
  30 + * 仿钉钉流程设计 Service 实现类
  31 + *
  32 + * @author jason
  33 + */
  34 +@Service
  35 +@Validated
  36 +public class BpmSimpleModelServiceImpl implements BpmSimpleModelService {
  37 +
  38 + @Resource
  39 + private BpmModelService bpmModelService;
  40 +
  41 + @Override
  42 + public Boolean saveSimpleModel(BpmSimpleModelSaveReqVO reqVO) {
  43 + Model model = bpmModelService.getModel(reqVO.getModelId());
  44 + if (model == null) {
  45 + throw exception(MODEL_NOT_EXISTS);
  46 + }
  47 +// byte[] bpmnBytes = bpmModelService.getModelBpmnXML(reqVO.getModelId());
  48 +// if (ArrayUtil.isEmpty(bpmnBytes)) {
  49 +// // BPMN XML 不存在。新增
  50 +// BpmnModel bpmnModel = BpmnModelUtils.convertSimpleModelToBpmnModel(model.getKey(), model.getName(), reqVO.getSimpleModelBody());
  51 +// bpmModelService.saveModelBpmnXml(model.getId(), BpmnModelUtils.getBpmnXml(bpmnModel));
  52 +// return Boolean.TRUE;
  53 +// } else {
  54 +// // TODO BPMN XML 已经存在。如何修改 ?? TODO add by 芋艿:感觉一个流程,只能二选一,要么 bpmn、要么 simple
  55 +// return Boolean.FALSE;
  56 +// }
  57 + // 1. JSON 转换成 bpmnModel
  58 + BpmnModel bpmnModel = BpmnModelUtils.convertSimpleModelToBpmnModel(model.getKey(), model.getName(), reqVO.getSimpleModelBody());
  59 + // 2.1 保存 Bpmn XML
  60 + bpmModelService.saveModelBpmnXml(model.getId(), StrUtil.utf8Bytes(BpmnModelUtils.getBpmnXml(bpmnModel)));
  61 + // 2.2 保存 JSON 数据
  62 + bpmModelService.saveModelSimpleJson(model.getId(), JsonUtils.toJsonByte(reqVO.getSimpleModelBody()));
  63 + return Boolean.TRUE;
  64 + }
  65 +
  66 + @Override
  67 + public BpmSimpleModelNodeVO getSimpleModel(String modelId) {
  68 + Model model = bpmModelService.getModel(modelId);
  69 + if (model == null) {
  70 + throw exception(MODEL_NOT_EXISTS);
  71 + }
  72 + // 暂时不用 bpmn 转 json, 有点复杂,
  73 + // 通过 ACT_RE_MODEL 表 EDITOR_SOURCE_EXTRA_VALUE_ID_ 获取 仿钉钉快搭模型的JSON 数据
  74 + byte[] jsonBytes = bpmModelService.getModelSimpleJson(model.getId());
  75 + return JsonUtils.parseObject(jsonBytes, BpmSimpleModelNodeVO.class);
  76 + }
  77 +
  78 + // TODO @jason:一般要支持这个么?感觉 bpmn 转 json 支持会不会太复杂。可以优先级低一点,做下调研~
  79 +
  80 + /**
  81 + * Bpmn Model 转换成 仿钉钉流程设计模型数据结构(json) 待完善
  82 + *
  83 + * @param bpmnModel Bpmn Model
  84 + * @return 仿钉钉流程设计模型数据结构
  85 + */
  86 + private BpmSimpleModelNodeVO convertBpmnModelToSimpleModel(BpmnModel bpmnModel) {
  87 + if (bpmnModel == null) {
  88 + return null;
  89 + }
  90 + StartEvent startEvent = BpmnModelUtils.getStartEvent(bpmnModel);
  91 + if (startEvent == null) {
  92 + return null;
  93 + }
  94 + BpmSimpleModelNodeVO rootNode = new BpmSimpleModelNodeVO();
  95 + rootNode.setType(START_EVENT_NODE.getType());
  96 + rootNode.setId(startEvent.getId());
  97 + rootNode.setName(startEvent.getName());
  98 + recursiveBuildSimpleModelNode(startEvent, rootNode);
  99 + return rootNode;
  100 + }
  101 +
  102 + private void recursiveBuildSimpleModelNode(FlowNode currentFlowNode, BpmSimpleModelNodeVO currentSimpleModeNode) {
  103 + BpmSimpleModelNodeType nodeType = BpmSimpleModelNodeType.valueOf(currentSimpleModeNode.getType());
  104 + Assert.notNull(nodeType, "节点类型不支持");
  105 + // 校验节点是否支持转仿钉钉的流程模型
  106 + List<SequenceFlow> outgoingFlows = validateCanConvertSimpleNode(nodeType, currentFlowNode);
  107 + if (CollUtil.isEmpty(outgoingFlows) || outgoingFlows.get(0).getTargetFlowElement() == null) {
  108 + return;
  109 + }
  110 + FlowElement targetElement = outgoingFlows.get(0).getTargetFlowElement();
  111 + // 如果是 EndEvent 直接退出
  112 + if (targetElement instanceof EndEvent) {
  113 + return;
  114 + }
  115 + if (targetElement instanceof UserTask) {
  116 + BpmSimpleModelNodeVO childNode = convertUserTaskToSimpleModelNode((UserTask) targetElement);
  117 + currentSimpleModeNode.setChildNode(childNode);
  118 + recursiveBuildSimpleModelNode((FlowNode) targetElement, childNode);
  119 + }
  120 + // TODO 其它节点类型待实现
  121 + }
  122 +
  123 + private BpmSimpleModelNodeVO convertUserTaskToSimpleModelNode(UserTask userTask) {
  124 + BpmSimpleModelNodeVO simpleModelNodeVO = new BpmSimpleModelNodeVO();
  125 + simpleModelNodeVO.setType(BpmSimpleModelNodeType.APPROVE_USER_NODE.getType());
  126 + simpleModelNodeVO.setName(userTask.getName());
  127 + simpleModelNodeVO.setId(userTask.getId());
  128 + Map<String, Object> attributes = MapUtil.newHashMap();
  129 + // TODO 暂时是普通审批,需要加会签
  130 + attributes.put("approveMethod", 1);
  131 + attributes.computeIfAbsent(USER_TASK_CANDIDATE_STRATEGY, (key) -> BpmnModelUtils.parseCandidateStrategy(userTask));
  132 + attributes.computeIfAbsent(USER_TASK_CANDIDATE_PARAM, (key) -> BpmnModelUtils.parseCandidateParam(userTask));
  133 + simpleModelNodeVO.setAttributes(attributes);
  134 + return simpleModelNodeVO;
  135 + }
  136 +
  137 + private List<SequenceFlow> validateCanConvertSimpleNode(BpmSimpleModelNodeType nodeType, FlowNode currentFlowNode) {
  138 + switch (nodeType) {
  139 + case START_EVENT_NODE:
  140 + case APPROVE_USER_NODE: {
  141 + List<SequenceFlow> outgoingFlows = currentFlowNode.getOutgoingFlows();
  142 + if (CollUtil.isNotEmpty(outgoingFlows) && outgoingFlows.size() > 1) {
  143 + throw exception(CONVERT_TO_SIMPLE_MODEL_NOT_SUPPORT);
  144 + }
  145 + validIsSupportFlowNode(outgoingFlows.get(0).getTargetFlowElement());
  146 + return outgoingFlows;
  147 + }
  148 + default: {
  149 + // TODO 其它节点类型待实现
  150 + throw exception(CONVERT_TO_SIMPLE_MODEL_NOT_SUPPORT);
  151 + }
  152 + }
  153 + }
  154 +
  155 + private void validIsSupportFlowNode(FlowElement targetElement) {
  156 + if (targetElement == null) {
  157 + return;
  158 + }
  159 + boolean isSupport = false;
  160 + for (Class<? extends FlowNode> item : BpmnModelConstants.SUPPORT_CONVERT_SIMPLE_FlOW_NODES) {
  161 + if (item.isInstance(targetElement)) {
  162 + isSupport = true;
  163 + break;
  164 + }
  165 + }
  166 + if (!isSupport) {
  167 + throw exception(CONVERT_TO_SIMPLE_MODEL_NOT_SUPPORT);
  168 + }
  169 + }
  170 +}
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyService.java
... ... @@ -17,9 +17,11 @@ public interface BpmProcessInstanceCopyService {
17 17 * 流程实例的抄送
18 18 *
19 19 * @param userIds 抄送的用户编号
20   - * @param taskId 流程任务编号
  20 + * @param processInstanceId 流程编号
  21 + * @param taskId 任务编号
  22 + * @param taskName 任务名称
21 23 */
22   - void createProcessInstanceCopy(Collection<Long> userIds, String taskId);
  24 + void createProcessInstanceCopy(Collection<Long> userIds, String processInstanceId, String taskId, String taskName);
23 25  
24 26 /**
25 27 * 获得抄送的流程的分页
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyServiceImpl.java
1 1 package cn.iocoder.yudao.module.bpm.service.task;
2 2  
3   -import cn.hutool.core.util.ObjectUtil;
4 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 4 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCopyPageReqVO;
6 5 import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO;
... ... @@ -11,7 +10,6 @@ import jakarta.annotation.Resource;
11 10 import lombok.extern.slf4j.Slf4j;
12 11 import org.flowable.engine.repository.ProcessDefinition;
13 12 import org.flowable.engine.runtime.ProcessInstance;
14   -import org.flowable.task.api.Task;
15 13 import org.springframework.context.annotation.Lazy;
16 14 import org.springframework.stereotype.Service;
17 15 import org.springframework.validation.annotation.Validated;
... ... @@ -47,14 +45,14 @@ public class BpmProcessInstanceCopyServiceImpl implements BpmProcessInstanceCopy
47 45 private BpmProcessDefinitionService processDefinitionService;
48 46  
49 47 @Override
50   - public void createProcessInstanceCopy(Collection<Long> userIds, String taskId) {
51   - // 1.1 校验任务存在
52   - Task task = taskService.getTask(taskId);
53   - if (ObjectUtil.isNull(task)) {
54   - throw exception(ErrorCodeConstants.TASK_NOT_EXISTS);
55   - }
  48 + public void createProcessInstanceCopy(Collection<Long> userIds, String processInstanceId, String taskId, String taskName) {
  49 + // 1.1 校验任务存在 暂时去掉这个校验. 因为任务可能仿钉钉快搭的抄送节点(ScriptTask)
  50 +// Task task = taskService.getTask(taskId);
  51 +// if (ObjectUtil.isNull(task)) {
  52 +// throw exception(ErrorCodeConstants.TASK_NOT_EXISTS);
  53 +// }
56 54 // 1.2 校验流程实例存在
57   - String processInstanceId = task.getProcessInstanceId();
  55 +// String processInstanceId = task.getProcessInstanceId();
58 56 ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId);
59 57 if (processInstance == null) {
60 58 throw exception(ErrorCodeConstants.PROCESS_INSTANCE_NOT_EXISTS);
... ... @@ -70,7 +68,7 @@ public class BpmProcessInstanceCopyServiceImpl implements BpmProcessInstanceCopy
70 68 List<BpmProcessInstanceCopyDO> copyList = convertList(userIds, userId -> new BpmProcessInstanceCopyDO()
71 69 .setUserId(userId).setStartUserId(Long.valueOf(processInstance.getStartUserId()))
72 70 .setProcessInstanceId(processInstanceId).setProcessInstanceName(processInstance.getName())
73   - .setCategory(processDefinition.getCategory()).setTaskId(taskId).setTaskName(task.getName()));
  71 + .setCategory(processDefinition.getCategory()).setTaskId(taskId).setTaskName(taskName));
74 72 processInstanceCopyMapper.insertBatch(copyList);
75 73 }
76 74  
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmSimpleNodeService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.service.task;
  2 +
  3 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
  4 +import jakarta.annotation.Resource;
  5 +import org.flowable.bpmn.model.FlowElement;
  6 +import org.flowable.engine.delegate.DelegateExecution;
  7 +import org.springframework.stereotype.Service;
  8 +
  9 +import java.util.Set;
  10 +
  11 +/**
  12 + * 仿钉钉快搭各个节点 Service
  13 + * @author jason
  14 + */
  15 +@Service
  16 +public class BpmSimpleNodeService {
  17 +
  18 + @Resource
  19 + private BpmTaskCandidateInvoker taskCandidateInvoker;
  20 + @Resource
  21 + private BpmProcessInstanceCopyService processInstanceCopyService;
  22 +
  23 + /**
  24 + * 仿钉钉快搭抄送
  25 + * @param execution 执行的任务(ScriptTask)
  26 + */
  27 + public Boolean copy(DelegateExecution execution) {
  28 + Set<Long> userIds = taskCandidateInvoker.calculateUsers(execution);
  29 + FlowElement currentFlowElement = execution.getCurrentFlowElement();
  30 + processInstanceCopyService.createProcessInstanceCopy(userIds, execution.getProcessInstanceId(),
  31 + currentFlowElement.getId(), currentFlowElement.getName());
  32 + return Boolean.TRUE;
  33 + }
  34 +}
... ...
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
... ... @@ -186,7 +186,8 @@ public class BpmTaskServiceImpl implements BpmTaskService {
186 186  
187 187 // 2. 抄送用户
188 188 if (CollUtil.isNotEmpty(reqVO.getCopyUserIds())) {
189   - processInstanceCopyService.createProcessInstanceCopy(reqVO.getCopyUserIds(), reqVO.getId());
  189 + processInstanceCopyService.createProcessInstanceCopy(reqVO.getCopyUserIds(), instance.getProcessInstanceId(),
  190 + reqVO.getId(), task.getName());
190 191 }
191 192  
192 193 // 情况一:被委派的任务,不调用 complete 去完成任务
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsCustomerController.http
... ... @@ -53,3 +53,13 @@ tenant-id: {{adminTenentId}}
53 53 GET {{baseUrl}}/crm/statistics-customer/get-customer-deal-cycle-by-user?deptId=100&times[0]=2023-01-01 00:00:00&times[1]=2024-12-12 23:59:59
54 54 Authorization: Bearer {{token}}
55 55 tenant-id: {{adminTenentId}}
  56 +
  57 +### 6.3 获取客户成交周期(按区域)
  58 +GET {{baseUrl}}/crm/statistics-customer/get-customer-deal-cycle-by-area?deptId=100&times[0]=2023-01-01 00:00:00&times[1]=2024-12-12 23:59:59
  59 +Authorization: Bearer {{token}}
  60 +tenant-id: {{adminTenentId}}
  61 +
  62 +### 6.4 获取客户成交周期(按产品)
  63 +GET {{baseUrl}}/crm/statistics-customer/get-customer-deal-cycle-by-product?deptId=100&times[0]=2023-01-01 00:00:00&times[1]=2024-12-12 23:59:59
  64 +Authorization: Bearer {{token}}
  65 +tenant-id: {{adminTenentId}}
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsCustomerController.java
... ... @@ -96,6 +96,18 @@ public class CrmStatisticsCustomerController {
96 96 return success(customerService.getCustomerDealCycleByUser(reqVO));
97 97 }
98 98  
99   - // TODO dhb52:【成交周期分析】里,有按照员工(已实现)、地区(未实现)、产品(未实现),需要在看看哈;可以把 CustomerDealCycle 拆成 3 个 tab,员工客户成交周期分析、地区客户成交周期分析、产品客户成交周期分析;
  99 + @GetMapping("/get-customer-deal-cycle-by-area")
  100 + @Operation(summary = "获取客户成交周期(按用户)")
  101 + @PreAuthorize("@ss.hasPermission('crm:statistics-customer:query')")
  102 + public CommonResult<List<CrmStatisticsCustomerDealCycleByAreaRespVO>> getCustomerDealCycleByArea(@Valid CrmStatisticsCustomerReqVO reqVO) {
  103 + return success(customerService.getCustomerDealCycleByArea(reqVO));
  104 + }
  105 +
  106 + @GetMapping("/get-customer-deal-cycle-by-product")
  107 + @Operation(summary = "获取客户成交周期(按用户)")
  108 + @PreAuthorize("@ss.hasPermission('crm:statistics-customer:query')")
  109 + public CommonResult<List<CrmStatisticsCustomerDealCycleByProductRespVO>> getCustomerDealCycleByProduct(@Valid CrmStatisticsCustomerReqVO reqVO) {
  110 + return success(customerService.getCustomerDealCycleByProduct(reqVO));
  111 + }
100 112  
101 113 }
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerDealCycleByAreaRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonIgnore;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +
  7 +@Schema(description = "管理后台 - CRM 客户成交周期分析(按区域) VO")
  8 +@Data
  9 +public class CrmStatisticsCustomerDealCycleByAreaRespVO {
  10 +
  11 + @Schema(description = "省份编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  12 + @JsonIgnore
  13 + private Integer areaId;
  14 +
  15 + @Schema(description = "省份名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "浙江省")
  16 + private String areaName;
  17 +
  18 + @Schema(description = "成交周期", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0")
  19 + private Double customerDealCycle;
  20 +
  21 + @Schema(description = "成交客户数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  22 + private Integer customerDealCount;
  23 +
  24 +}
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/vo/customer/CrmStatisticsCustomerDealCycleByProductRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +@Schema(description = "管理后台 - CRM 客户成交周期分析(按产品) VO")
  7 +@Data
  8 +public class CrmStatisticsCustomerDealCycleByProductRespVO {
  9 +
  10 + @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "演示产品")
  11 + private String productName;
  12 +
  13 + @Schema(description = "成交周期", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0")
  14 + private Double customerDealCycle;
  15 +
  16 + @Schema(description = "成交客户数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  17 + private Integer customerDealCount;
  18 +
  19 +}
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/statistics/CrmStatisticsCustomerMapper.java
... ... @@ -53,6 +53,7 @@ public interface CrmStatisticsCustomerMapper {
53 53  
54 54 /**
55 55 * 合同总金额(按用户)
  56 + *
56 57 * @return 统计数据@return 统计数据@param reqVO 请求参数
57 58 * @return 统计数据
58 59 */
... ... @@ -191,4 +192,20 @@ public interface CrmStatisticsCustomerMapper {
191 192 */
192 193 List<CrmStatisticsCustomerDealCycleByUserRespVO> selectCustomerDealCycleGroupByUser(CrmStatisticsCustomerReqVO reqVO);
193 194  
  195 + /**
  196 + * 客户成交周期(按区域)
  197 + *
  198 + * @param reqVO 请求参数
  199 + * @return 统计数据
  200 + */
  201 + List<CrmStatisticsCustomerDealCycleByAreaRespVO> selectCustomerDealCycleGroupByAreaId(CrmStatisticsCustomerReqVO reqVO);
  202 +
  203 + /**
  204 + * 客户成交周期(按产品)
  205 + *
  206 + * @param reqVO 请求参数
  207 + * @return 统计数据
  208 + */
  209 + List<CrmStatisticsCustomerDealCycleByProductRespVO> selectCustomerDealCycleGroupByProductId(CrmStatisticsCustomerReqVO reqVO);
  210 +
194 211 }
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsCustomerService.java
... ... @@ -77,7 +77,7 @@ public interface CrmStatisticsCustomerService {
77 77  
78 78 /**
79 79 * 客户成交周期(按日期)
80   - *
  80 + * <p>
81 81 * 成交周期的定义:客户 customer 在创建出来,到合同 contract 第一次成交的时间差
82 82 *
83 83 * @param reqVO 请求参数
... ... @@ -93,4 +93,20 @@ public interface CrmStatisticsCustomerService {
93 93 */
94 94 List<CrmStatisticsCustomerDealCycleByUserRespVO> getCustomerDealCycleByUser(CrmStatisticsCustomerReqVO reqVO);
95 95  
  96 + /**
  97 + * 客户成交周期(按区域)
  98 + *
  99 + * @param reqVO 请求参数
  100 + * @return 统计数据
  101 + */
  102 + List<CrmStatisticsCustomerDealCycleByAreaRespVO> getCustomerDealCycleByArea(CrmStatisticsCustomerReqVO reqVO);
  103 +
  104 + /**
  105 + * 客户成交周期(按产品)
  106 + *
  107 + * @param reqVO 请求参数
  108 + * @return 统计数据
  109 + */
  110 + List<CrmStatisticsCustomerDealCycleByProductRespVO> getCustomerDealCycleByProduct(CrmStatisticsCustomerReqVO reqVO);
  111 +
96 112 }
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/statistics/CrmStatisticsCustomerServiceImpl.java
... ... @@ -4,6 +4,9 @@ import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.util.ObjUtil;
5 5 import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
6 6 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
  7 +import cn.iocoder.yudao.framework.ip.core.Area;
  8 +import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
  9 +import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
7 10 import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.*;
8 11 import cn.iocoder.yudao.module.crm.dal.mysql.statistics.CrmStatisticsCustomerMapper;
9 12 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
... ... @@ -19,6 +22,7 @@ import java.time.LocalDateTime;
19 22 import java.util.Collections;
20 23 import java.util.List;
21 24 import java.util.Map;
  25 +import java.util.function.Function;
22 26 import java.util.stream.Stream;
23 27  
24 28 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
... ... @@ -290,6 +294,51 @@ public class CrmStatisticsCustomerServiceImpl implements CrmStatisticsCustomerSe
290 294 return summaryList;
291 295 }
292 296  
  297 + @Override
  298 + public List<CrmStatisticsCustomerDealCycleByAreaRespVO> getCustomerDealCycleByArea(CrmStatisticsCustomerReqVO reqVO) {
  299 + // 1. 获得用户编号数组
  300 + List<Long> userIds = getUserIds(reqVO);
  301 + if (CollUtil.isEmpty(userIds)) {
  302 + return Collections.emptyList();
  303 + }
  304 + reqVO.setUserIds(userIds);
  305 +
  306 + // 2. 获取客户地区统计数据
  307 + List<CrmStatisticsCustomerDealCycleByAreaRespVO> dealCycleByAreaList = customerMapper.selectCustomerDealCycleGroupByAreaId(reqVO);
  308 + if (CollUtil.isEmpty(dealCycleByAreaList)) {
  309 + return Collections.emptyList();
  310 + }
  311 +
  312 + // 3. 拼接数据
  313 + Map<Integer, Area> areaMap = convertMap(AreaUtils.getByType(AreaTypeEnum.PROVINCE, Function.identity()),
  314 + Area::getId);
  315 + return convertList(dealCycleByAreaList, vo -> {
  316 + if (vo.getAreaId() != null) {
  317 + Integer parentId = AreaUtils.getParentIdByType(vo.getAreaId(), AreaTypeEnum.PROVINCE);
  318 + findAndThen(areaMap, parentId, area -> vo.setAreaId(parentId).setAreaName(area.getName()));
  319 + }
  320 + return vo;
  321 + });
  322 + }
  323 +
  324 + @Override
  325 + public List<CrmStatisticsCustomerDealCycleByProductRespVO> getCustomerDealCycleByProduct(CrmStatisticsCustomerReqVO reqVO) {
  326 + // 1. 获得用户编号数组
  327 + List<Long> userIds = getUserIds(reqVO);
  328 + if (CollUtil.isEmpty(userIds)) {
  329 + return Collections.emptyList();
  330 + }
  331 + reqVO.setUserIds(userIds);
  332 +
  333 + // 2. 获取客户产品统计数据
  334 + List<CrmStatisticsCustomerDealCycleByProductRespVO> dealCycleByProductList = customerMapper.selectCustomerDealCycleGroupByProductId(reqVO);
  335 + if (CollUtil.isEmpty(dealCycleByProductList)) {
  336 + return Collections.emptyList();
  337 + }
  338 +
  339 + return dealCycleByProductList;
  340 + }
  341 +
293 342 /**
294 343 * 拼接用户信息(昵称)
295 344 *
... ...
yudao-module-crm/yudao-module-crm-biz/src/main/resources/mapper/statistics/CrmStatisticsCustomerMapper.xml
... ... @@ -19,17 +19,18 @@
19 19 <!-- TODO 芋艿:应该不用过滤时间 -->
20 20 <select id="selectCustomerDealCountGroupByDate"
21 21 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByDateRespVO">
22   - SELECT DATE_FORMAT(customer.create_time, '%Y-%m-%d') AS time,
23   - COUNT(DISTINCT customer.id) AS customer_deal_count
  22 + SELECT DATE_FORMAT(customer.create_time, '%Y-%m-%d') AS time,
  23 + COUNT(DISTINCT customer.id) AS customer_deal_count
24 24 FROM crm_customer AS customer
25 25 LEFT JOIN crm_contract AS contract ON contract.customer_id = customer.id
26   - WHERE customer.deleted = 0 AND contract.deleted = 0
  26 + WHERE customer.deleted = 0
  27 + AND contract.deleted = 0
27 28 AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
28 29 AND customer.owner_user_id IN
29 30 <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
30 31 #{userId}
31 32 </foreach>
32   - AND contract.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
  33 + AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
33 34 GROUP BY time
34 35 </select>
35 36  
... ... @@ -53,13 +54,14 @@
53 54 COUNT(DISTINCT customer.id) AS customer_deal_count
54 55 FROM crm_customer AS customer
55 56 LEFT JOIN crm_contract AS contract ON contract.customer_id = customer.id
56   - WHERE customer.deleted = 0 AND contract.deleted = 0
  57 + WHERE customer.deleted = 0
  58 + AND contract.deleted = 0
57 59 AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
58 60 AND customer.owner_user_id IN
59 61 <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
60 62 #{userId}
61 63 </foreach>
62   - AND contract.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
  64 + AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
63 65 GROUP BY customer.owner_user_id
64 66 </select>
65 67  
... ... @@ -221,4 +223,42 @@
221 223 GROUP BY customer.owner_user_id
222 224 </select>
223 225  
  226 + <select id="selectCustomerDealCycleGroupByAreaId"
  227 + resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerDealCycleByAreaRespVO">
  228 + SELECT customer.area_id AS area_id,
  229 + IFNULL(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0) AS customer_deal_cycle,
  230 + COUNT(DISTINCT customer.id) AS customer_deal_count
  231 + FROM crm_customer AS customer
  232 + LEFT JOIN crm_contract AS contract ON customer.id = contract.customer_id
  233 + WHERE customer.deleted = 0
  234 + AND contract.deleted = 0
  235 + AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
  236 + AND customer.owner_user_id IN
  237 + <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
  238 + #{userId}
  239 + </foreach>
  240 + AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
  241 + GROUP BY
  242 + customer.area_id
  243 + </select>
  244 +
  245 + <select id="selectCustomerDealCycleGroupByProductId"
  246 + resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerDealCycleByProductRespVO">
  247 + SELECT (SELECT name FROM crm_product WHERE id = product.id) AS product_name,
  248 + IFNULL(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0) AS customer_deal_cycle,
  249 + COUNT(DISTINCT customer.id) AS customer_deal_count
  250 + FROM crm_customer AS customer
  251 + LEFT JOIN crm_contract AS contract ON customer.id = contract.customer_id
  252 + LEFT JOIN crm_contract_product AS product ON product.contract_id = contract.id
  253 + WHERE customer.deleted = 0
  254 + AND contract.deleted = 0
  255 + AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
  256 + AND customer.owner_user_id IN
  257 + <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
  258 + #{userId}
  259 + </foreach>
  260 + AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
  261 + GROUP BY product.id
  262 + </select>
  263 +
224 264 </mapper>
... ...