list2 = BeanUtils.toBean(page.getList(),
+// BpmProcessInstanceCCPageItemRespVO.class,
+// copy -> {
+// MapUtils.findAndThen(userMap, Long.valueOf(copy.getCreator()), copy::setCreatorNickname);
+// MapUtils.findAndThen(userMap, copy.getStartUserId(), copy::setStartUserNickname);
+// MapUtils.findAndThen(taskMap, copy.getTaskId(), copy::setTaskName);
+// MapUtils.findAndThen(processInstaneMap, copy.getProcessInstanceId(), copy::setProcessInstanceName);
+// });
+ return new PageResult<>(list, page.getTotal());
+ }
+
+}
diff --git a/yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/dal/dataobject/member/MemberStatisticsDO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/cc/BpmProcessInstanceCopyDO.java
index af980b5..3bd2874 100644
--- a/yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/dal/dataobject/member/MemberStatisticsDO.java
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/cc/BpmProcessInstanceCopyDO.java
@@ -1,70 +1,71 @@
-package cn.iocoder.yudao.module.statistics.dal.dataobject.member;
+package cn.iocoder.yudao.module.bpm.dal.dataobject.cc;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
-import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
-import java.time.LocalDateTime;
-
/**
- * 会员统计 DO
- *
- * 以天为维度,统计全部的数据
+ * 流程抄送 DO
*
- * @author 芋道源码
+ * @author kyle
+ * @date 2022-05-19 TODO @kyle:@date 不是标准 java doc,可以使用 @since 替代,然后日期是不是不对
*/
-@TableName("member_statistics")
-@KeySequence("member_statistics_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@TableName(value = "bpm_process_instance_copy", autoResultMap = true)
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
-public class MemberStatisticsDO extends BaseDO {
+public class BpmProcessInstanceCopyDO extends BaseDO {
/**
- * 编号,主键自增
+ * 编号
*/
@TableId
private Long id;
+ // TODO @kyle:字段如果是关联或者冗余,要写下注释。以 processInstanceId 举例子。
/**
- * 统计日期
+ * 发起人 Id
*/
- private LocalDateTime time;
-
+ private Long startUserId;
/**
- * 注册用户数量
+ * 流程名
*/
- private Integer userRegisterCount;
+ private String processInstanceName;
/**
- * 访问用户数量(UV)
+ * 流程实例的编号
+ *
+ * 关联 ProcessInstance 的 id 属性
*/
- private Integer userVisitCount;
+ private String processInstanceId;
+
/**
- * 访问页面数量(PV)
+ * 任务主键
*/
- private Integer pageVisitCount;
+ private String taskId;
/**
- * 充值用户数量
+ * 任务名称
*/
- private Integer rechargeUserCount;
+ private String taskName;
/**
- * 创建订单用户数
+ * 用户编号
*/
- private Integer orderCreateUserCount;
+ private Long userId;
+
/**
- * 支付订单用户数
+ * 抄送原因
*/
- private Integer orderPayUserCount;
+ private String reason;
+
+ // TODO @kyle:这个字段,可以用 category 简化点
/**
- * 总支付金额,单位:分
+ * 流程分类
*/
- private Integer orderPayPrice;
+ private String processDefinitionCategory;
}
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/cc/BpmProcessInstanceCopyMapper.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/cc/BpmProcessInstanceCopyMapper.java
new file mode 100644
index 0000000..3b88484
--- /dev/null
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/cc/BpmProcessInstanceCopyMapper.java
@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.bpm.dal.mysql.cc;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCCMyPageReqVO;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.cc.BpmProcessInstanceCopyDO;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface BpmProcessInstanceCopyMapper extends BaseMapperX { // TODO @kyle:方法和类之间要空行下;
+ default PageResult selectPage(Long loginUserId, BpmProcessInstanceCCMyPageReqVO reqVO){
+ return selectPage(reqVO, new LambdaQueryWrapperX()
+ .eqIfPresent(BpmProcessInstanceCopyDO::getUserId, loginUserId)
+ .eqIfPresent(BpmProcessInstanceCopyDO::getProcessInstanceId, reqVO.getProcessInstanceId())
+ .likeIfPresent(BpmProcessInstanceCopyDO::getProcessInstanceName, reqVO.getProcessInstanceName())
+ .betweenIfPresent(BpmProcessInstanceCopyDO::getCreateTime, reqVO.getCreateTime())
+ .orderByDesc(BpmProcessInstanceCopyDO::getId));
+ }
+}
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/bpm/config/BpmCandidateProcessorConfiguration.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/bpm/config/BpmCandidateProcessorConfiguration.java
new file mode 100644
index 0000000..cff0348
--- /dev/null
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/bpm/config/BpmCandidateProcessorConfiguration.java
@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.module.bpm.framework.bpm.config;
+
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.script.BpmTaskAssignScript;
+import cn.iocoder.yudao.module.bpm.service.candidate.sourceInfoProcessor.*;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+// TODO @芋艿:Candidate 相关还在完善中,用户可以暂时忽略,仅 yudao 开发的同学需要关注~计划是把 Candidate 和 Assign 融合成一套
+/**
+ * BPM 通用的 Configuration 配置类,提供给 Activiti 和 Flowable
+ * @author kyle
+ */
+@Configuration(proxyBeanMethods = false)
+public class BpmCandidateProcessorConfiguration {
+ @Bean
+ public BpmCandidateAdminUserApiSourceInfoProcessor bpmCandidateAdminUserApiSourceInfoProcessor() {
+ return new BpmCandidateAdminUserApiSourceInfoProcessor();
+ }
+
+ @Bean
+ public BpmCandidateDeptApiSourceInfoProcessor bpmCandidateDeptApiSourceInfoProcessor() {
+ return new BpmCandidateDeptApiSourceInfoProcessor();
+ }
+
+ @Bean
+ public BpmCandidatePostApiSourceInfoProcessor bpmCandidatePostApiSourceInfoProcessor() {
+ return new BpmCandidatePostApiSourceInfoProcessor();
+ }
+
+ @Bean
+ public BpmCandidateRoleApiSourceInfoProcessor bpmCandidateRoleApiSourceInfoProcessor() {
+ return new BpmCandidateRoleApiSourceInfoProcessor();
+ }
+
+ @Bean
+ public BpmCandidateUserGroupApiSourceInfoProcessor bpmCandidateUserGroupApiSourceInfoProcessor() {
+ return new BpmCandidateUserGroupApiSourceInfoProcessor();
+ }
+
+ /**
+ * 可以自己定制脚本,然后通过这里设置到处理器里面去
+ * @param scriptsOp 脚本包装对象
+ * @return
+ */
+ @Bean
+ public BpmCandidateScriptApiSourceInfoProcessor bpmCandidateScriptApiSourceInfoProcessor(ObjectProvider scriptsOp) {
+ BpmCandidateScriptApiSourceInfoProcessor bpmCandidateScriptApiSourceInfoProcessor = new BpmCandidateScriptApiSourceInfoProcessor();
+ bpmCandidateScriptApiSourceInfoProcessor.setScripts(scriptsOp);
+ return bpmCandidateScriptApiSourceInfoProcessor;
+ }
+}
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/BpmCandidateSourceInfo.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/BpmCandidateSourceInfo.java
new file mode 100644
index 0000000..8582ad9
--- /dev/null
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/BpmCandidateSourceInfo.java
@@ -0,0 +1,50 @@
+package cn.iocoder.yudao.module.bpm.service.candidate;
+
+import cn.iocoder.yudao.module.bpm.controller.admin.candidate.vo.BpmTaskCandidateRuleVO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 获取候选人信息
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class BpmCandidateSourceInfo {
+
+ @Schema(description = "流程id")
+ @NotNull
+ private String processInstanceId;
+
+ @Schema(description = "当前任务ID")
+ @NotNull
+ private String taskId;
+ /**
+ * 通过这些规则,生成最终需要生成的用户
+ */
+ @Schema(description = "当前任务预选规则")
+ @NotEmpty(message = "不允许空规则")
+ private Set rules;
+
+ @Schema(description = "发起抄送的用户")
+ private String creator;
+
+ @Schema(description = "抄送原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "请帮忙审查下!")
+ @NotEmpty(message = "抄送原因不能为空")
+ private String reason;
+
+ public void addRule(BpmTaskCandidateRuleVO vo) {
+ assert vo != null;
+ if (rules == null) {
+ rules = new HashSet<>();
+ }
+ rules.add(vo);
+ }
+}
\ No newline at end of file
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/BpmCandidateSourceInfoProcessor.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/BpmCandidateSourceInfoProcessor.java
new file mode 100644
index 0000000..0fe741c
--- /dev/null
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/BpmCandidateSourceInfoProcessor.java
@@ -0,0 +1,53 @@
+package cn.iocoder.yudao.module.bpm.service.candidate;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.module.bpm.controller.admin.candidate.vo.BpmTaskCandidateRuleVO;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmTaskAssignRuleTypeEnum;
+import org.flowable.engine.delegate.DelegateExecution;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+public interface BpmCandidateSourceInfoProcessor {
+ /**
+ * 获取该处理器支持的类型
+ * 来自 {@link BpmTaskAssignRuleTypeEnum}
+ *
+ * @return
+ */
+ Set getSupportedTypes();
+
+ /**
+ * 对规则和人员做校验
+ *
+ * @param type 规则
+ * @param options 人员id
+ */
+ void validRuleOptions(Integer type, Set options);
+
+ /**
+ * 默认的处理
+ * 如果想去操作所有的规则,则可以覆盖此方法
+ *
+ * @param request 原始请求
+ * @param delegateExecution 审批过程中的对象
+ * @return 必须包含的是用户ID,而不是其他的ID
+ * @throws Exception
+ */
+ default Set process(BpmCandidateSourceInfo request, DelegateExecution delegateExecution) throws Exception {
+ Set rules = request.getRules();
+ Set results = new HashSet<>();
+ for (BpmTaskCandidateRuleVO rule : rules) {
+ // 每个处理器都有机会处理自己支持的事件
+ if (CollUtil.contains(getSupportedTypes(), rule.getType())) {
+ results.addAll(doProcess(request, rule, delegateExecution));
+ }
+ }
+ return results;
+ }
+
+ default Set doProcess(BpmCandidateSourceInfo request, BpmTaskCandidateRuleVO rule, DelegateExecution delegateExecution) {
+ return Collections.emptySet();
+ }
+}
\ No newline at end of file
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/BpmCandidateSourceInfoProcessorChain.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/BpmCandidateSourceInfoProcessorChain.java
new file mode 100644
index 0000000..dafc083
--- /dev/null
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/BpmCandidateSourceInfoProcessorChain.java
@@ -0,0 +1,107 @@
+package cn.iocoder.yudao.module.bpm.service.candidate;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.module.bpm.controller.admin.candidate.vo.BpmTaskCandidateRuleVO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.stereotype.Service;
+
+import jakarta.annotation.Resource;
+import java.util.*;
+
+@Service
+public class BpmCandidateSourceInfoProcessorChain {
+
+ // 保存处理节点
+
+ private List processorList;
+ @Resource
+ private AdminUserApi adminUserApi;
+
+ /**
+ * 可添加其他处理器
+ *
+ * @param processorOp
+ * @return
+ */
+ @Resource
+ // 动态扩展处理节点
+ public BpmCandidateSourceInfoProcessorChain addProcessor(ObjectProvider processorOp) {
+ List processor = ListUtil.toList(processorOp.iterator());
+ if (null == processorList) {
+ processorList = new ArrayList<>(processor.size());
+ }
+ processorList.addAll(processor);
+ return this;
+ }
+
+ // 获取处理器处理
+ public Set process(BpmCandidateSourceInfo sourceInfo, DelegateExecution execution) throws Exception {
+ // Verify our parameters
+ if (sourceInfo == null) {
+ throw new IllegalArgumentException();
+ }
+ for (BpmCandidateSourceInfoProcessor processor : processorList) {
+ try {
+ for (BpmTaskCandidateRuleVO vo : sourceInfo.getRules()) {
+ if (CollUtil.contains(processor.getSupportedTypes(), vo.getType())) {
+ processor.validRuleOptions(vo.getType(), vo.getOptions());
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw e;
+ }
+ }
+ Set saveResult = Collections.emptySet();
+ Exception saveException = null;
+ for (BpmCandidateSourceInfoProcessor processor : processorList) {
+ try {
+ saveResult = processor.process(sourceInfo, execution);
+ if (CollUtil.isNotEmpty(saveResult)) {
+ removeDisableUsers(saveResult);
+ break;
+ }
+ } catch (Exception e) {
+ saveException = e;
+ break;
+ }
+ }
+ // Return the exception or result state from the last execute()
+ if ((saveException != null)) {
+ throw saveException;
+ } else {
+ return (saveResult);
+ }
+ }
+
+ public Set calculateTaskCandidateUsers(DelegateExecution execution, BpmCandidateSourceInfo sourceInfo) {
+ Set results = Collections.emptySet();
+ try {
+ results = process(sourceInfo, execution);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return results;
+ }
+
+ /**
+ * 移除禁用用户
+ *
+ * @param assigneeUserIds
+ */
+ public void removeDisableUsers(Set assigneeUserIds) {
+ if (CollUtil.isEmpty(assigneeUserIds)) {
+ return;
+ }
+ Map userMap = adminUserApi.getUserMap(assigneeUserIds);
+ assigneeUserIds.removeIf(id -> {
+ AdminUserRespDTO user = userMap.get(id);
+ return user == null || !CommonStatusEnum.ENABLE.getStatus().equals(user.getStatus());
+ });
+ }
+}
\ No newline at end of file
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/sourceInfoProcessor/BpmCandidateAdminUserApiSourceInfoProcessor.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/sourceInfoProcessor/BpmCandidateAdminUserApiSourceInfoProcessor.java
new file mode 100644
index 0000000..536b3ee
--- /dev/null
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/sourceInfoProcessor/BpmCandidateAdminUserApiSourceInfoProcessor.java
@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.bpm.service.candidate.sourceInfoProcessor;
+
+import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
+import cn.iocoder.yudao.module.bpm.controller.admin.candidate.vo.BpmTaskCandidateRuleVO;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmTaskAssignRuleTypeEnum;
+import cn.iocoder.yudao.module.bpm.service.candidate.BpmCandidateSourceInfo;
+import cn.iocoder.yudao.module.bpm.service.candidate.BpmCandidateSourceInfoProcessor;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import org.flowable.engine.delegate.DelegateExecution;
+
+import jakarta.annotation.Resource;
+import java.util.Set;
+
+public class BpmCandidateAdminUserApiSourceInfoProcessor implements BpmCandidateSourceInfoProcessor {
+ @Resource
+ private AdminUserApi api;
+
+ @Override
+ public Set getSupportedTypes() {
+ return SetUtils.asSet(BpmTaskAssignRuleTypeEnum.USER.getType());
+ }
+
+ @Override
+ public void validRuleOptions(Integer type, Set options) {
+ api.validateUserList(options);
+ }
+
+ @Override
+ public Set doProcess(BpmCandidateSourceInfo request, BpmTaskCandidateRuleVO rule, DelegateExecution delegateExecution) {
+ return rule.getOptions();
+ }
+}
\ No newline at end of file
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/sourceInfoProcessor/BpmCandidateDeptApiSourceInfoProcessor.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/sourceInfoProcessor/BpmCandidateDeptApiSourceInfoProcessor.java
new file mode 100644
index 0000000..6fcfa23
--- /dev/null
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/sourceInfoProcessor/BpmCandidateDeptApiSourceInfoProcessor.java
@@ -0,0 +1,50 @@
+package cn.iocoder.yudao.module.bpm.service.candidate.sourceInfoProcessor;
+
+import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
+import cn.iocoder.yudao.module.bpm.controller.admin.candidate.vo.BpmTaskCandidateRuleVO;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmTaskAssignRuleTypeEnum;
+import cn.iocoder.yudao.module.bpm.service.candidate.BpmCandidateSourceInfo;
+import cn.iocoder.yudao.module.bpm.service.candidate.BpmCandidateSourceInfoProcessor;
+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 org.flowable.engine.delegate.DelegateExecution;
+
+import jakarta.annotation.Resource;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+
+public class BpmCandidateDeptApiSourceInfoProcessor implements BpmCandidateSourceInfoProcessor {
+ @Resource
+ private DeptApi api;
+ @Resource
+ private AdminUserApi adminUserApi;
+
+ @Override
+ public Set getSupportedTypes() {
+ return SetUtils.asSet(BpmTaskAssignRuleTypeEnum.DEPT_MEMBER.getType(),
+ BpmTaskAssignRuleTypeEnum.DEPT_LEADER.getType());
+ }
+
+ @Override
+ public void validRuleOptions(Integer type, Set options) {
+ api.validateDeptList(options);
+ }
+
+ @Override
+ public Set doProcess(BpmCandidateSourceInfo request, BpmTaskCandidateRuleVO rule, DelegateExecution delegateExecution) {
+ if (Objects.equals(BpmTaskAssignRuleTypeEnum.DEPT_MEMBER.getType(), rule.getType())) {
+ List users = adminUserApi.getUserListByDeptIds(rule.getOptions());
+ return convertSet(users, AdminUserRespDTO::getId);
+ } else if (Objects.equals(BpmTaskAssignRuleTypeEnum.DEPT_LEADER.getType(), rule.getType())) {
+ List depts = api.getDeptList(rule.getOptions());
+ return convertSet(depts, DeptRespDTO::getLeaderUserId);
+ }
+ return Collections.emptySet();
+ }
+}
\ No newline at end of file
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/sourceInfoProcessor/BpmCandidatePostApiSourceInfoProcessor.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/sourceInfoProcessor/BpmCandidatePostApiSourceInfoProcessor.java
new file mode 100644
index 0000000..f924d87
--- /dev/null
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/sourceInfoProcessor/BpmCandidatePostApiSourceInfoProcessor.java
@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.module.bpm.service.candidate.sourceInfoProcessor;
+
+import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
+import cn.iocoder.yudao.module.bpm.controller.admin.candidate.vo.BpmTaskCandidateRuleVO;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmTaskAssignRuleTypeEnum;
+import cn.iocoder.yudao.module.bpm.service.candidate.BpmCandidateSourceInfo;
+import cn.iocoder.yudao.module.bpm.service.candidate.BpmCandidateSourceInfoProcessor;
+import cn.iocoder.yudao.module.system.api.dept.PostApi;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import org.flowable.engine.delegate.DelegateExecution;
+
+import jakarta.annotation.Resource;
+import java.util.List;
+import java.util.Set;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+
+public class BpmCandidatePostApiSourceInfoProcessor implements BpmCandidateSourceInfoProcessor {
+ @Resource
+ private PostApi api;
+ @Resource
+ private AdminUserApi adminUserApi;
+
+ @Override
+ public Set getSupportedTypes() {
+ return SetUtils.asSet(BpmTaskAssignRuleTypeEnum.POST.getType());
+ }
+
+ @Override
+ public void validRuleOptions(Integer type, Set options) {
+ api.validPostList(options);
+ }
+
+ @Override
+ public Set doProcess(BpmCandidateSourceInfo request, BpmTaskCandidateRuleVO rule, DelegateExecution delegateExecution) {
+ List users = adminUserApi.getUserListByPostIds(rule.getOptions());
+ return convertSet(users, AdminUserRespDTO::getId);
+ }
+}
\ No newline at end of file
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/sourceInfoProcessor/BpmCandidateRoleApiSourceInfoProcessor.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/sourceInfoProcessor/BpmCandidateRoleApiSourceInfoProcessor.java
new file mode 100644
index 0000000..f3ba5b9
--- /dev/null
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/sourceInfoProcessor/BpmCandidateRoleApiSourceInfoProcessor.java
@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.bpm.service.candidate.sourceInfoProcessor;
+
+import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
+import cn.iocoder.yudao.module.bpm.controller.admin.candidate.vo.BpmTaskCandidateRuleVO;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmTaskAssignRuleTypeEnum;
+import cn.iocoder.yudao.module.bpm.service.candidate.BpmCandidateSourceInfo;
+import cn.iocoder.yudao.module.bpm.service.candidate.BpmCandidateSourceInfoProcessor;
+import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
+import cn.iocoder.yudao.module.system.api.permission.RoleApi;
+import org.flowable.engine.delegate.DelegateExecution;
+
+import jakarta.annotation.Resource;
+import java.util.Set;
+
+public class BpmCandidateRoleApiSourceInfoProcessor implements BpmCandidateSourceInfoProcessor {
+ @Resource
+ private RoleApi api;
+
+ @Resource
+ private PermissionApi permissionApi;
+
+ @Override
+ public Set getSupportedTypes() {
+ return SetUtils.asSet(BpmTaskAssignRuleTypeEnum.ROLE.getType());
+ }
+
+ @Override
+ public void validRuleOptions(Integer type, Set options) {
+ api.validRoleList(options);
+ }
+
+ @Override
+ public Set doProcess(BpmCandidateSourceInfo request, BpmTaskCandidateRuleVO rule, DelegateExecution delegateExecution) {
+ return permissionApi.getUserRoleIdListByRoleIds(rule.getOptions());
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/sourceInfoProcessor/BpmCandidateScriptApiSourceInfoProcessor.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/sourceInfoProcessor/BpmCandidateScriptApiSourceInfoProcessor.java
new file mode 100644
index 0000000..4bf2569
--- /dev/null
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/sourceInfoProcessor/BpmCandidateScriptApiSourceInfoProcessor.java
@@ -0,0 +1,73 @@
+package cn.iocoder.yudao.module.bpm.service.candidate.sourceInfoProcessor;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
+import cn.iocoder.yudao.module.bpm.controller.admin.candidate.vo.BpmTaskCandidateRuleVO;
+import cn.iocoder.yudao.module.bpm.enums.DictTypeConstants;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmTaskAssignRuleTypeEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.script.BpmTaskAssignScript;
+import cn.iocoder.yudao.module.bpm.service.candidate.BpmCandidateSourceInfo;
+import cn.iocoder.yudao.module.bpm.service.candidate.BpmCandidateSourceInfoProcessor;
+import cn.iocoder.yudao.module.system.api.dict.DictDataApi;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.springframework.beans.factory.ObjectProvider;
+
+import jakarta.annotation.Resource;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
+import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.TASK_ASSIGN_SCRIPT_NOT_EXISTS;
+
+public class BpmCandidateScriptApiSourceInfoProcessor implements BpmCandidateSourceInfoProcessor {
+ @Resource
+ private DictDataApi dictDataApi;
+
+ /**
+ * 任务分配脚本
+ */
+ private Map scriptMap = Collections.emptyMap();
+
+ public void setScripts(ObjectProvider scriptsOp) {
+ List scripts = scriptsOp.orderedStream().collect(Collectors.toList());
+ setScripts(scripts);
+ }
+
+ public void setScripts(List scripts) {
+ this.scriptMap = convertMap(scripts, script -> script.getEnum().getId());
+ }
+
+ @Override
+ public Set getSupportedTypes() {
+ return SetUtils.asSet(BpmTaskAssignRuleTypeEnum.SCRIPT.getType());
+ }
+
+ @Override
+ public void validRuleOptions(Integer type, Set options) {
+ dictDataApi.validateDictDataList(DictTypeConstants.TASK_ASSIGN_SCRIPT,
+ CollectionUtils.convertSet(options, String::valueOf));
+ }
+
+ @Override
+ public Set doProcess(BpmCandidateSourceInfo request, BpmTaskCandidateRuleVO rule, DelegateExecution delegateExecution) {
+ return calculateTaskCandidateUsersByScript(delegateExecution, rule.getOptions());
+ }
+
+ private Set calculateTaskCandidateUsersByScript(DelegateExecution execution, Set options) {
+ // 获得对应的脚本
+ List scripts = new ArrayList<>(options.size());
+ options.forEach(id -> {
+ BpmTaskAssignScript script = scriptMap.get(id);
+ if (script == null) {
+ throw exception(TASK_ASSIGN_SCRIPT_NOT_EXISTS, id);
+ }
+ scripts.add(script);
+ });
+ // 逐个计算任务
+ Set userIds = new HashSet<>();
+ scripts.forEach(script -> CollUtil.addAll(userIds, script.calculateTaskCandidateUsers(execution)));
+ return userIds;
+ }
+}
\ No newline at end of file
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/sourceInfoProcessor/BpmCandidateUserGroupApiSourceInfoProcessor.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/sourceInfoProcessor/BpmCandidateUserGroupApiSourceInfoProcessor.java
new file mode 100644
index 0000000..b0720b0
--- /dev/null
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/candidate/sourceInfoProcessor/BpmCandidateUserGroupApiSourceInfoProcessor.java
@@ -0,0 +1,41 @@
+package cn.iocoder.yudao.module.bpm.service.candidate.sourceInfoProcessor;
+
+import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
+import cn.iocoder.yudao.module.bpm.controller.admin.candidate.vo.BpmTaskCandidateRuleVO;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmUserGroupDO;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmTaskAssignRuleTypeEnum;
+import cn.iocoder.yudao.module.bpm.service.candidate.BpmCandidateSourceInfo;
+import cn.iocoder.yudao.module.bpm.service.candidate.BpmCandidateSourceInfoProcessor;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmUserGroupService;
+import org.flowable.engine.delegate.DelegateExecution;
+
+import jakarta.annotation.Resource;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class BpmCandidateUserGroupApiSourceInfoProcessor implements BpmCandidateSourceInfoProcessor {
+ @Resource
+ private BpmUserGroupService api;
+ @Resource
+ private BpmUserGroupService userGroupService;
+
+ @Override
+ public Set getSupportedTypes() {
+ return SetUtils.asSet(BpmTaskAssignRuleTypeEnum.USER_GROUP.getType());
+ }
+
+ @Override
+ public void validRuleOptions(Integer type, Set options) {
+ api.validUserGroups(options);
+ }
+
+ @Override
+ public Set doProcess(BpmCandidateSourceInfo request, BpmTaskCandidateRuleVO rule, DelegateExecution delegateExecution) {
+ List userGroups = userGroupService.getUserGroupList(rule.getOptions());
+ Set userIds = new HashSet<>();
+ userGroups.forEach(group -> userIds.addAll(group.getMemberUserIds()));
+ return userIds;
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/cc/BpmProcessInstanceCopyService.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/cc/BpmProcessInstanceCopyService.java
new file mode 100644
index 0000000..799656f
--- /dev/null
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/cc/BpmProcessInstanceCopyService.java
@@ -0,0 +1,53 @@
+package cn.iocoder.yudao.module.bpm.service.cc;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCCMyPageReqVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCCPageItemRespVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCCReqVO;
+import cn.iocoder.yudao.module.bpm.service.candidate.BpmCandidateSourceInfo;
+
+// TODO @kyle:这个 Service 要不挪到 task 包下;保持统一,task 下有流程、任务、抄送等;
+// TODO @kyle:中文和英文之间,有个空格,会更清晰点;例如说;流程抄送 Service 接口;中文写作习惯~
+/**
+ * 流程抄送Service接口
+ *
+ * 现在是在审批的时候进行流程抄送
+ */
+public interface BpmProcessInstanceCopyService {
+
+ // TODO @kyle:无用的方法,可以去掉哈;另外,考虑到避免过多的 VO,这里就可以返回 BpmProcessInstanceCopyDO
+ /**
+ * 查询流程抄送
+ *
+ * @param copyId 流程抄送主键
+ * @return 流程抄送
+ */
+ BpmProcessInstanceCopyVO queryById(Long copyId);
+
+ // TODO 芋艿:这块要 review 下;思考下~~
+ /**
+ * 抄送
+ * @param sourceInfo 抄送源信息,方便抄送处理
+ * @return
+ */
+ boolean makeCopy(BpmCandidateSourceInfo sourceInfo);
+
+ // TODO @kyle:可以方法名改成 createProcessInstanceCopy;现在项目一般新增都用 create 为主;
+ /**
+ * 流程实例的抄送
+ *
+ * @param userId 当前登录用户
+ * @param createReqVO 创建的抄送请求
+ * @return 是否抄送成功,抄送成功则返回true TODO @kyle:这里可以不用返回哈;目前一般是失败,就抛出业务异常;
+ */
+ boolean ccProcessInstance(Long userId, BpmProcessInstanceCCReqVO createReqVO);
+
+ /**
+ * 抄送的流程
+ * @param loginUserId 登录用户id
+ * @param pageReqVO 分页请求
+ * @return 抄送的分页结果
+ */
+ PageResult getMyProcessInstanceCCPage(Long loginUserId,
+ BpmProcessInstanceCCMyPageReqVO pageReqVO);
+}
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/cc/BpmProcessInstanceCopyServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/cc/BpmProcessInstanceCopyServiceImpl.java
new file mode 100644
index 0000000..edc5f7f
--- /dev/null
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/cc/BpmProcessInstanceCopyServiceImpl.java
@@ -0,0 +1,165 @@
+package cn.iocoder.yudao.module.bpm.service.cc;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCCMyPageReqVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCCPageItemRespVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCCReqVO;
+import cn.iocoder.yudao.module.bpm.convert.cc.BpmProcessInstanceCopyConvert;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.cc.BpmProcessInstanceCopyDO;
+import cn.iocoder.yudao.module.bpm.dal.mysql.cc.BpmProcessInstanceCopyMapper;
+import cn.iocoder.yudao.module.bpm.service.candidate.BpmCandidateSourceInfo;
+import cn.iocoder.yudao.module.bpm.service.candidate.BpmCandidateSourceInfoProcessorChain;
+import cn.iocoder.yudao.module.bpm.service.cc.dto.BpmDelegateExecutionDTO;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
+import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
+import cn.iocoder.yudao.module.bpm.util.FlowableUtils;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.flowable.engine.RuntimeService;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.flowable.engine.repository.ProcessDefinition;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
+// TODO @kyle:类注释要写下
+@Slf4j // TODO @kyle:按照 @Service、@Validated、@Slf4j,从重要到不重要的顺序;
+@Service
+@Validated
+public class BpmProcessInstanceCopyServiceImpl implements BpmProcessInstanceCopyService {
+ @Resource // TODO @kyle:第一个变量,和类之间要有空行;
+ private BpmProcessInstanceCopyMapper processInstanceCopyMapper;
+
+ /**
+ * 和flowable有关的,查询流程名用的 TODO @kyle:可以不写哈注释;
+ */
+ @Resource
+ private RuntimeService runtimeService;
+
+ /**
+ * 找抄送人用的 TODO @kyle:可以不写哈注释;
+ */
+ @Resource
+ private BpmCandidateSourceInfoProcessorChain processorChain;
+
+ // TODO @kyle:多余的变量,可以去掉哈
+ @Resource
+ @Lazy // 解决循环依赖
+ private BpmTaskService bpmTaskService;
+ @Resource
+ @Lazy // 解决循环依赖
+ private BpmProcessInstanceService bpmProcessInstanceService;
+ @Resource
+ private AdminUserApi adminUserApi;
+
+ @Override
+ public BpmProcessInstanceCopyVO queryById(Long copyId) {
+ BpmProcessInstanceCopyDO bpmProcessInstanceCopyDO = processInstanceCopyMapper.selectById(copyId);
+ return BpmProcessInstanceCopyConvert.INSTANCE.convert(bpmProcessInstanceCopyDO);
+ }
+
+ // TODO @kyle:makeCopy 和 ccProcessInstance 的调用关系,感受上反了;
+ // makeCopy 有点像基于规则,查找抄送人,然后创建;
+ // ccProcessInstance 是已经有了抄送人,然后创建;
+ // 建议的改造:独立基于 processInstanceCopyMapper 做 insert
+ @Override
+ public boolean makeCopy(BpmCandidateSourceInfo sourceInfo) {
+ if (null == sourceInfo) {
+ return false;
+ }
+
+ DelegateExecution executionEntity = new BpmDelegateExecutionDTO(sourceInfo.getProcessInstanceId());
+ Set ccCandidates = processorChain.calculateTaskCandidateUsers(executionEntity, sourceInfo);
+ if (CollUtil.isEmpty(ccCandidates)) {
+ log.warn("相关抄送人不存在 {}", sourceInfo.getTaskId());
+ return false;
+ } else {
+ BpmProcessInstanceCopyDO copyDO = new BpmProcessInstanceCopyDO();
+ // 调用
+ // 设置任务id
+ copyDO.setTaskId(sourceInfo.getTaskId());
+ copyDO.setTaskName(FlowableUtils.getTaskNameByTaskId(sourceInfo.getTaskId()));
+ copyDO.setProcessInstanceId(sourceInfo.getProcessInstanceId());
+ ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
+ .processInstanceId(sourceInfo.getProcessInstanceId())
+ .singleResult();
+ if (null == processInstance) {
+ log.warn("相关流程实例不存在 {}", sourceInfo.getTaskId());
+ return false;
+ }
+ copyDO.setStartUserId(FlowableUtils.getStartUserIdFromProcessInstance(processInstance));
+ copyDO.setProcessInstanceName(processInstance.getName());
+ ProcessDefinition processDefinition = FlowableUtils.getProcessDefinition(processInstance.getProcessDefinitionId());
+ copyDO.setProcessDefinitionCategory(processDefinition.getCategory());
+ copyDO.setReason(sourceInfo.getReason());
+ copyDO.setCreator(sourceInfo.getCreator());
+ copyDO.setCreateTime(LocalDateTime.now());
+ List copyList = new ArrayList<>(ccCandidates.size());
+ for (Long userId : ccCandidates) {
+ BpmProcessInstanceCopyDO copy = BpmProcessInstanceCopyConvert.INSTANCE.copy(copyDO);
+ copy.setUserId(userId);
+ copyList.add(copy);
+ }
+ return processInstanceCopyMapper.insertBatch(copyList);
+ }
+ }
+
+ @Override
+ public boolean ccProcessInstance(Long userId, BpmProcessInstanceCCReqVO reqVO) {
+ // 在能正常审批的情况下抄送流程
+ BpmCandidateSourceInfo sourceInfo = new BpmCandidateSourceInfo();
+ sourceInfo.setTaskId(reqVO.getTaskKey());
+ sourceInfo.setProcessInstanceId(reqVO.getProcessInstanceKey());
+ sourceInfo.addRule(reqVO);
+ sourceInfo.setCreator(String.valueOf(userId));
+ sourceInfo.setReason(reqVO.getReason());
+ if (!makeCopy(sourceInfo)) {
+ throw new RuntimeException("抄送任务失败");
+ }
+ return false;
+ }
+
+ //获取流程抄送分页 TODO @kyle:接口已经注释,这里不用注释了哈;
+ @Override
+ public PageResult getMyProcessInstanceCCPage(Long loginUserId, BpmProcessInstanceCCMyPageReqVO pageReqVO) {
+ // 通过 BpmProcessInstanceExtDO 表,先查询到对应的分页
+ // TODO @kyle:一般读逻辑,Service 返回 PageResult 即可。关联数据的查询和拼接,交给 Controller;目的是:保证 Service 聚焦写逻辑,清晰简洁;
+ PageResult pageResult = processInstanceCopyMapper.selectPage(loginUserId, pageReqVO);
+ if (CollUtil.isEmpty(pageResult.getList())) {
+ return new PageResult<>(pageResult.getTotal());
+ }
+
+ // TODO @kyle:这种可以简洁点;参考如下
+// Map processInstanceMap = bpmProcessInstanceService.getProcessInstanceMap(
+// convertSet(pageResult.getList(), BpmProcessInstanceCopyDO::getProcessInstanceId));
+
+ Set taskIds = new HashSet<>();
+ Set processInstaneIds = new HashSet<>();
+ Set userIds = new HashSet<>();
+ for (BpmProcessInstanceCopyDO doItem : pageResult.getList()) {
+ taskIds.add(doItem.getTaskId());
+ processInstaneIds.add(doItem.getProcessInstanceId());
+ userIds.add(doItem.getStartUserId());
+ Long userId = Long.valueOf(doItem.getCreator());
+ userIds.add(userId);
+ }
+
+ Map taskNameByTaskIds = FlowableUtils.getTaskNameByTaskIds(taskIds);
+ Map processInstanceNameByTaskIds = FlowableUtils.getProcessInstanceNameByTaskIds(processInstaneIds);
+
+ Map userMap = adminUserApi.getUserList(userIds).stream().collect(Collectors.toMap(
+ AdminUserRespDTO::getId, AdminUserRespDTO::getNickname));
+
+ // 转换返回
+ return BpmProcessInstanceCopyConvert.INSTANCE.convertPage(pageResult, taskNameByTaskIds, processInstanceNameByTaskIds, userMap);
+ }
+
+}
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/cc/BpmProcessInstanceCopyVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/cc/BpmProcessInstanceCopyVO.java
new file mode 100644
index 0000000..881ec6f
--- /dev/null
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/cc/BpmProcessInstanceCopyVO.java
@@ -0,0 +1,67 @@
+package cn.iocoder.yudao.module.bpm.service.cc;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+// TODO @kyle:看看是不是要删除
+/**
+ * 流程抄送视图对象 wf_copy
+ *
+ * @author ruoyi
+ * @date 2022-05-19
+ */
+@Data
+public class BpmProcessInstanceCopyVO {
+
+ /**
+ * 编号
+ */
+ @Schema(description = "抄送主键")
+ private Long id;
+
+ /**
+ * 发起人Id
+ */
+ @Schema(description = "发起人Id")
+ private Long startUserId;
+
+ @Schema(description = "发起人别名")
+ private String startUserNickname;
+
+ /**
+ * 流程主键
+ */
+ @Schema(description = "流程实例的主键")
+ private String processInstanceId;
+
+ @Schema(description = "流程实例的名字")
+ private String processInstanceName;
+
+ /**
+ * 任务主键
+ */
+ @Schema(description = "发起抄送的任务编号")
+ private String taskId;
+
+ @Schema(description = "发起抄送的任务名称")
+ private String taskName;
+ /**
+ * 用户主键
+ */
+ @Schema(description = "用户编号")
+ private Long userId;
+
+ @Schema(description = "用户别名")
+ private Long userNickname;
+
+ @Schema(description = "抄送原因")
+ private String reason;
+
+ @Schema(description = "抄送人")
+ private String creator;
+
+ @Schema(description = "抄送时间")
+ private LocalDateTime createTime;
+}
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/cc/dto/BpmDelegateExecutionDTO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/cc/dto/BpmDelegateExecutionDTO.java
new file mode 100644
index 0000000..47213ae
--- /dev/null
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/cc/dto/BpmDelegateExecutionDTO.java
@@ -0,0 +1,439 @@
+package cn.iocoder.yudao.module.bpm.service.cc.dto;
+
+import org.flowable.bpmn.model.FlowElement;
+import org.flowable.bpmn.model.FlowableListener;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.flowable.engine.delegate.ReadOnlyDelegateExecution;
+import org.flowable.variable.api.persistence.entity.VariableInstance;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 仅为了传输processInstanceId
+ */
+public class BpmDelegateExecutionDTO implements DelegateExecution {
+
+ public BpmDelegateExecutionDTO(String getProcessInstanceId) {
+ this.getProcessInstanceId = getProcessInstanceId;
+ }
+
+ private final String getProcessInstanceId;
+
+ @Override
+ public String getId() {
+ return null;
+ }
+
+ @Override
+ public String getProcessInstanceId() {
+ return null;
+ }
+
+ @Override
+ public String getRootProcessInstanceId() {
+ return null;
+ }
+
+ @Override
+ public String getEventName() {
+ return null;
+ }
+
+ @Override
+ public void setEventName(String eventName) {
+
+ }
+
+ @Override
+ public String getProcessInstanceBusinessKey() {
+ return null;
+ }
+
+ @Override
+ public String getProcessInstanceBusinessStatus() {
+ return null;
+ }
+
+ @Override
+ public String getProcessDefinitionId() {
+ return null;
+ }
+
+ @Override
+ public String getPropagatedStageInstanceId() {
+ return null;
+ }
+
+ @Override
+ public String getParentId() {
+ return null;
+ }
+
+ @Override
+ public String getSuperExecutionId() {
+ return null;
+ }
+
+ @Override
+ public String getCurrentActivityId() {
+ return null;
+ }
+
+ @Override
+ public String getTenantId() {
+ return null;
+ }
+
+ @Override
+ public FlowElement getCurrentFlowElement() {
+ return null;
+ }
+
+ @Override
+ public void setCurrentFlowElement(FlowElement flowElement) {
+
+ }
+
+ @Override
+ public FlowableListener getCurrentFlowableListener() {
+ return null;
+ }
+
+ @Override
+ public void setCurrentFlowableListener(FlowableListener currentListener) {
+
+ }
+
+ @Override
+ public ReadOnlyDelegateExecution snapshotReadOnly() {
+ return null;
+ }
+
+ @Override
+ public DelegateExecution getParent() {
+ return null;
+ }
+
+ @Override
+ public List extends DelegateExecution> getExecutions() {
+ return null;
+ }
+
+ @Override
+ public void setActive(boolean isActive) {
+
+ }
+
+ @Override
+ public boolean isActive() {
+ return false;
+ }
+
+ @Override
+ public boolean isEnded() {
+ return false;
+ }
+
+ @Override
+ public void setConcurrent(boolean isConcurrent) {
+
+ }
+
+ @Override
+ public boolean isConcurrent() {
+ return false;
+ }
+
+ @Override
+ public boolean isProcessInstanceType() {
+ return false;
+ }
+
+ @Override
+ public void inactivate() {
+
+ }
+
+ @Override
+ public boolean isScope() {
+ return false;
+ }
+
+ @Override
+ public void setScope(boolean isScope) {
+
+ }
+
+ @Override
+ public boolean isMultiInstanceRoot() {
+ return false;
+ }
+
+ @Override
+ public void setMultiInstanceRoot(boolean isMultiInstanceRoot) {
+
+ }
+
+ @Override
+ public Map getVariables() {
+ return null;
+ }
+
+ @Override
+ public Map getVariableInstances() {
+ return null;
+ }
+
+ @Override
+ public Map getVariables(Collection collection) {
+ return null;
+ }
+
+ @Override
+ public Map getVariableInstances(Collection collection) {
+ return null;
+ }
+
+ @Override
+ public Map getVariables(Collection collection, boolean b) {
+ return null;
+ }
+
+ @Override
+ public Map getVariableInstances(Collection collection, boolean b) {
+ return null;
+ }
+
+ @Override
+ public Map getVariablesLocal() {
+ return null;
+ }
+
+ @Override
+ public Map getVariableInstancesLocal() {
+ return null;
+ }
+
+ @Override
+ public Map getVariablesLocal(Collection collection) {
+ return null;
+ }
+
+ @Override
+ public Map getVariableInstancesLocal(Collection collection) {
+ return null;
+ }
+
+ @Override
+ public Map getVariablesLocal(Collection collection, boolean b) {
+ return null;
+ }
+
+ @Override
+ public Map getVariableInstancesLocal(Collection collection, boolean b) {
+ return null;
+ }
+
+ @Override
+ public Object getVariable(String s) {
+ return null;
+ }
+
+ @Override
+ public VariableInstance getVariableInstance(String s) {
+ return null;
+ }
+
+ @Override
+ public Object getVariable(String s, boolean b) {
+ return null;
+ }
+
+ @Override
+ public VariableInstance getVariableInstance(String s, boolean b) {
+ return null;
+ }
+
+ @Override
+ public Object getVariableLocal(String s) {
+ return null;
+ }
+
+ @Override
+ public VariableInstance getVariableInstanceLocal(String s) {
+ return null;
+ }
+
+ @Override
+ public Object getVariableLocal(String s, boolean b) {
+ return null;
+ }
+
+ @Override
+ public VariableInstance getVariableInstanceLocal(String s, boolean b) {
+ return null;
+ }
+
+ @Override
+ public T getVariable(String s, Class aClass) {
+ return null;
+ }
+
+ @Override
+ public T getVariableLocal(String s, Class aClass) {
+ return null;
+ }
+
+ @Override
+ public Set getVariableNames() {
+ return null;
+ }
+
+ @Override
+ public Set getVariableNamesLocal() {
+ return null;
+ }
+
+ @Override
+ public void setVariable(String s, Object o) {
+
+ }
+
+ @Override
+ public void setVariable(String s, Object o, boolean b) {
+
+ }
+
+ @Override
+ public Object setVariableLocal(String s, Object o) {
+ return null;
+ }
+
+ @Override
+ public Object setVariableLocal(String s, Object o, boolean b) {
+ return null;
+ }
+
+ @Override
+ public void setVariables(Map map) {
+
+ }
+
+ @Override
+ public void setVariablesLocal(Map map) {
+
+ }
+
+ @Override
+ public boolean hasVariables() {
+ return false;
+ }
+
+ @Override
+ public boolean hasVariablesLocal() {
+ return false;
+ }
+
+ @Override
+ public boolean hasVariable(String s) {
+ return false;
+ }
+
+ @Override
+ public boolean hasVariableLocal(String s) {
+ return false;
+ }
+
+ @Override
+ public void removeVariable(String s) {
+
+ }
+
+ @Override
+ public void removeVariableLocal(String s) {
+
+ }
+
+ @Override
+ public void removeVariables(Collection collection) {
+
+ }
+
+ @Override
+ public void removeVariablesLocal(Collection collection) {
+
+ }
+
+ @Override
+ public void removeVariables() {
+
+ }
+
+ @Override
+ public void removeVariablesLocal() {
+
+ }
+
+ @Override
+ public void setTransientVariable(String s, Object o) {
+
+ }
+
+ @Override
+ public void setTransientVariableLocal(String s, Object o) {
+
+ }
+
+ @Override
+ public void setTransientVariables(Map map) {
+
+ }
+
+ @Override
+ public Object getTransientVariable(String s) {
+ return null;
+ }
+
+ @Override
+ public Map getTransientVariables() {
+ return null;
+ }
+
+ @Override
+ public void setTransientVariablesLocal(Map map) {
+
+ }
+
+ @Override
+ public Object getTransientVariableLocal(String s) {
+ return null;
+ }
+
+ @Override
+ public Map getTransientVariablesLocal() {
+ return null;
+ }
+
+ @Override
+ public void removeTransientVariableLocal(String s) {
+
+ }
+
+ @Override
+ public void removeTransientVariable(String s) {
+
+ }
+
+ @Override
+ public void removeTransientVariables() {
+
+ }
+
+ @Override
+ public void removeTransientVariablesLocal() {
+
+ }
+}
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
index a0f938e..27a4c77 100644
--- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
@@ -19,6 +19,8 @@ import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceDeleteReasonEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceResultEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskAddSignTypeEnum;
+import cn.iocoder.yudao.module.bpm.service.candidate.BpmCandidateSourceInfo;
+import cn.iocoder.yudao.module.bpm.service.cc.BpmProcessInstanceCopyService;
import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService;
import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
@@ -94,6 +96,9 @@ public class BpmTaskServiceImpl implements BpmTaskService {
@Resource
private ManagementService managementService;
+ @Resource
+ private BpmProcessInstanceCopyService processInstanceCopyService;
+
@Override
public PageResult getTodoTaskPage(Long userId, BpmTaskTodoPageReqVO pageVO) {
// 查询待办任务
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/util/FlowableUtils.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/util/FlowableUtils.java
new file mode 100644
index 0000000..0217ea3
--- /dev/null
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/util/FlowableUtils.java
@@ -0,0 +1,127 @@
+package cn.iocoder.yudao.module.bpm.util;
+
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.extra.spring.SpringUtil;
+import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
+import org.flowable.bpmn.model.BpmnModel;
+import org.flowable.bpmn.model.ExtensionElement;
+import org.flowable.bpmn.model.FlowElement;
+import org.flowable.bpmn.model.FlowNode;
+import org.flowable.engine.RepositoryService;
+import org.flowable.engine.RuntimeService;
+import org.flowable.engine.TaskService;
+import org.flowable.engine.repository.ProcessDefinition;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.flowable.task.api.Task;
+
+import java.util.*;
+
+/**
+ * 流程引擎工具类封装
+ *
+ * @author: linjinp
+ * @create: 2019-12-24 13:51
+ **/
+public class FlowableUtils {
+
+ /**
+ * 获取流程名称
+ *
+ * @param processDefinitionId
+ * @return
+ */
+ public static String getProcessDefinitionName(String processDefinitionId) {
+ RepositoryService repositoryService = SpringUtil.getBean(RepositoryService.class);
+ ProcessDefinition processDefinition = repositoryService.getProcessDefinition(processDefinitionId);
+ return processDefinition.getName();
+ }
+
+ public static ProcessDefinition getProcessDefinition(String processDefinitionId) {
+ RepositoryService repositoryService = SpringUtil.getBean(RepositoryService.class);
+ return repositoryService.getProcessDefinition(processDefinitionId);
+ }
+
+ /**
+ * 获取节点数据
+ *
+ * @param processInstanceId
+ * @param nodeId
+ * @return
+ */
+ public static FlowNode getFlowNode(String processInstanceId, String nodeId) {
+
+ RuntimeService runtimeService = SpringUtil.getBean(RuntimeService.class);
+ RepositoryService repositoryService = SpringUtil.getBean(RepositoryService.class);
+
+ String definitionld = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult().getProcessDefinitionId(); // 获取bpm(模型)对象
+ BpmnModel bpmnModel = repositoryService.getBpmnModel(definitionld);
+ // 传节点定义key获取当前节点
+ FlowNode flowNode = (FlowNode) bpmnModel.getFlowElement(nodeId);
+ return flowNode;
+ }
+
+ public static ExtensionElement generateFlowNodeIdExtension(String nodeId) {
+ ExtensionElement extensionElement = new ExtensionElement();
+ extensionElement.setElementText(nodeId);
+ extensionElement.setName("nodeId");
+ extensionElement.setNamespacePrefix("flowable");
+ extensionElement.setNamespace("nodeId");
+ return extensionElement;
+ }
+
+ public static String getNodeIdFromExtension(FlowElement flowElement) {
+ Map> extensionElements = flowElement.getExtensionElements();
+ return extensionElements.get("nodeId").get(0).getElementText();
+ }
+
+ public static Long getStartUserIdFromProcessInstance(ProcessInstance instance) {
+ if (null == instance) {
+ return null;
+ }
+ return NumberUtils.parseLong(instance.getStartUserId());
+ }
+
+ public static String getTaskNameByTaskId(String taskId) {
+ TaskService taskService = SpringUtil.getBean(TaskService.class);
+ Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
+ return task.getName();
+ }
+
+ // TODO @kyle:Utils 里不做查询;可以封装到 bpmTaskService 里
+ public static Map getTaskNameByTaskIds(Collection taskIds) {
+ TaskService taskService = SpringUtil.getBean(TaskService.class);
+ List tasks = taskService.createTaskQuery().taskIds(taskIds).list();
+ if (CollUtil.isNotEmpty(tasks)) {
+ Map taskMap = new HashMap<>(tasks.size());
+ for (Task task : tasks) {
+ taskMap.putIfAbsent(task.getId(), task.getName());
+ }
+ return taskMap;
+ }
+ return Collections.emptyMap();
+ }
+
+ public static String getProcessInstanceNameByTaskId(String processInstanceId) {
+ RuntimeService runtimeService = SpringUtil.getBean(RuntimeService.class);
+ ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
+ .processInstanceId(processInstanceId)
+ .singleResult();
+ return processInstance.getName();
+ }
+
+ // TODO @kyle:Utils 里不做查询;可以封装到 bpmTaskService 里
+ public static Map getProcessInstanceNameByTaskIds(Set taskIds) {
+ RuntimeService runtimeService = SpringUtil.getBean(RuntimeService.class);
+ List processInstances = runtimeService.createProcessInstanceQuery().processInstanceIds(taskIds).list();
+ if (CollUtil.isNotEmpty(processInstances)) {
+ Map processInstaneMap = new HashMap<>(processInstances.size());
+ for (ProcessInstance processInstance : processInstances) {
+ processInstaneMap.putIfAbsent(processInstance.getId(), processInstance.getName());
+ }
+ return processInstaneMap;
+ }
+ return Collections.emptyMap();
+ }
+
+}
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/service/candidate/BpmCandidateSourceInfoProcessorChainTest.java b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/service/candidate/BpmCandidateSourceInfoProcessorChainTest.java
new file mode 100644
index 0000000..38eb7cb
--- /dev/null
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/service/candidate/BpmCandidateSourceInfoProcessorChainTest.java
@@ -0,0 +1,242 @@
+package cn.iocoder.yudao.module.bpm.service.candidate;
+
+import cn.hutool.core.map.MapUtil;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+import cn.iocoder.yudao.module.bpm.controller.admin.candidate.vo.BpmTaskCandidateRuleVO;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmUserGroupDO;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmTaskAssignRuleTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmTaskRuleScriptEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.script.BpmTaskAssignScript;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.script.impl.BpmTaskAssignLeaderX1Script;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.script.impl.BpmTaskAssignLeaderX2Script;
+import cn.iocoder.yudao.module.bpm.service.candidate.sourceInfoProcessor.BpmCandidateScriptApiSourceInfoProcessor;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmUserGroupService;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.PostApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.dict.DictDataApi;
+import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
+import cn.iocoder.yudao.module.system.api.permission.RoleApi;
+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.flowable.engine.delegate.DelegateExecution;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.context.annotation.Import;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static java.util.Collections.singleton;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+@Import({BpmCandidateSourceInfoProcessorChain.class,
+ BpmCandidateScriptApiSourceInfoProcessor.class, BpmTaskAssignLeaderX1Script.class,
+ BpmTaskAssignLeaderX2Script.class})
+public class BpmCandidateSourceInfoProcessorChainTest extends BaseDbUnitTest {
+ @Resource
+ private BpmCandidateSourceInfoProcessorChain processorChain;
+
+ @MockBean
+ private BpmUserGroupService userGroupService;
+ @MockBean
+ private DeptApi deptApi;
+ @MockBean
+ private AdminUserApi adminUserApi;
+ @MockBean
+ private PermissionApi permissionApi;
+ @MockBean
+ private RoleApi roleApi;
+ @MockBean
+ private PostApi postApi;
+ @MockBean
+ private DictDataApi dictDataApi;
+ @Resource
+ private BpmCandidateScriptApiSourceInfoProcessor bpmCandidateScriptApiSourceInfoProcessor;
+
+ @Test
+ public void testCalculateTaskCandidateUsers_Role() {
+ // 准备参数
+ BpmTaskCandidateRuleVO rule = new BpmTaskCandidateRuleVO().setOptions(asSet(1L, 2L))
+ .setType(BpmTaskAssignRuleTypeEnum.ROLE.getType());
+ // mock 方法
+ when(permissionApi.getUserRoleIdListByRoleIds(eq(rule.getOptions())))
+ .thenReturn(asSet(11L, 22L));
+ mockGetUserMap(asSet(11L, 22L));
+
+ // 调用
+ BpmCandidateSourceInfo sourceInfo = new BpmCandidateSourceInfo();
+ sourceInfo.addRule(rule);
+ Set results = processorChain.calculateTaskCandidateUsers(null, sourceInfo);
+ // 断言
+ assertEquals(asSet(11L, 22L), results);
+ }
+
+ @Test
+ public void testCalculateTaskCandidateUsers_DeptMember() {
+ // 准备参数
+ BpmTaskCandidateRuleVO rule = new BpmTaskCandidateRuleVO().setOptions(asSet(1L, 2L))
+ .setType(BpmTaskAssignRuleTypeEnum.DEPT_MEMBER.getType());
+ // mock 方法
+ List users = CollectionUtils.convertList(asSet(11L, 22L),
+ id -> new AdminUserRespDTO().setId(id));
+ when(adminUserApi.getUserListByDeptIds(eq(rule.getOptions()))).thenReturn(users);
+ mockGetUserMap(asSet(11L, 22L));
+
+ // 调用
+ BpmCandidateSourceInfo sourceInfo = new BpmCandidateSourceInfo();
+ sourceInfo.addRule(rule);
+ Set results = processorChain.calculateTaskCandidateUsers(null, sourceInfo);
+ // 断言
+ assertEquals(asSet(11L, 22L), results);
+ }
+
+ @Test
+ public void testCalculateTaskCandidateUsers_DeptLeader() {
+ // 准备参数
+ BpmTaskCandidateRuleVO rule = new BpmTaskCandidateRuleVO().setOptions(asSet(1L, 2L))
+ .setType(BpmTaskAssignRuleTypeEnum.DEPT_LEADER.getType());
+ // mock 方法
+ DeptRespDTO dept1 = randomPojo(DeptRespDTO.class, o -> o.setLeaderUserId(11L));
+ DeptRespDTO dept2 = randomPojo(DeptRespDTO.class, o -> o.setLeaderUserId(22L));
+ when(deptApi.getDeptList(eq(rule.getOptions()))).thenReturn(Arrays.asList(dept1, dept2));
+ mockGetUserMap(asSet(11L, 22L));
+
+ // 调用
+ BpmCandidateSourceInfo sourceInfo = new BpmCandidateSourceInfo();
+ sourceInfo.addRule(rule);
+ Set results = processorChain.calculateTaskCandidateUsers(null, sourceInfo);
+ // 断言
+ assertEquals(asSet(11L, 22L), results);
+ }
+
+ @Test
+ public void testCalculateTaskCandidateUsers_Post() {
+ // 准备参数
+ BpmTaskCandidateRuleVO rule = new BpmTaskCandidateRuleVO().setOptions(asSet(1L, 2L))
+ .setType(BpmTaskAssignRuleTypeEnum.POST.getType());
+ // mock 方法
+ List users = CollectionUtils.convertList(asSet(11L, 22L),
+ id -> new AdminUserRespDTO().setId(id));
+ when(adminUserApi.getUserListByPostIds(eq(rule.getOptions()))).thenReturn(users);
+ mockGetUserMap(asSet(11L, 22L));
+
+ // 调用
+ BpmCandidateSourceInfo sourceInfo = new BpmCandidateSourceInfo();
+ sourceInfo.addRule(rule);
+ Set results = processorChain.calculateTaskCandidateUsers(null, sourceInfo);
+ // 断言
+ assertEquals(asSet(11L, 22L), results);
+ }
+
+ @Test
+ public void testCalculateTaskCandidateUsers_User() {
+ // 准备参数
+ BpmTaskCandidateRuleVO rule = new BpmTaskCandidateRuleVO().setOptions(asSet(1L, 2L))
+ .setType(BpmTaskAssignRuleTypeEnum.USER.getType());
+ // mock 方法
+ mockGetUserMap(asSet(1L, 2L));
+
+ // 调用
+ BpmCandidateSourceInfo sourceInfo = new BpmCandidateSourceInfo();
+ sourceInfo.addRule(rule);
+ Set results = processorChain.calculateTaskCandidateUsers(null, sourceInfo);
+ // 断言
+ assertEquals(asSet(1L, 2L), results);
+ }
+
+ @Test
+ public void testCalculateTaskCandidateUsers_UserGroup() {
+ // 准备参数
+ BpmTaskCandidateRuleVO rule = new BpmTaskCandidateRuleVO().setOptions(asSet(1L, 2L))
+ .setType(BpmTaskAssignRuleTypeEnum.USER_GROUP.getType());
+ // mock 方法
+ BpmUserGroupDO userGroup1 = randomPojo(BpmUserGroupDO.class, o -> o.setMemberUserIds(asSet(11L, 12L)));
+ BpmUserGroupDO userGroup2 = randomPojo(BpmUserGroupDO.class, o -> o.setMemberUserIds(asSet(21L, 22L)));
+ when(userGroupService.getUserGroupList(eq(rule.getOptions()))).thenReturn(Arrays.asList(userGroup1, userGroup2));
+ mockGetUserMap(asSet(11L, 12L, 21L, 22L));
+
+ // 调用
+ BpmCandidateSourceInfo sourceInfo = new BpmCandidateSourceInfo();
+ sourceInfo.addRule(rule);
+ Set results = processorChain.calculateTaskCandidateUsers(null, sourceInfo);
+ // 断言
+ assertEquals(asSet(11L, 12L, 21L, 22L), results);
+ }
+
+ private void mockGetUserMap(Set assigneeUserIds) {
+ Map userMap = CollectionUtils.convertMap(assigneeUserIds, id -> id,
+ id -> new AdminUserRespDTO().setId(id).setStatus(CommonStatusEnum.ENABLE.getStatus()));
+ when(adminUserApi.getUserMap(eq(assigneeUserIds))).thenReturn(userMap);
+ }
+
+ @Test
+ public void testCalculateTaskCandidateUsers_Script() {
+ // 准备参数
+ BpmTaskCandidateRuleVO rule = new BpmTaskCandidateRuleVO().setOptions(asSet(20L, 21L))
+ .setType(BpmTaskAssignRuleTypeEnum.SCRIPT.getType());
+ // mock 方法
+ BpmTaskAssignScript script1 = new BpmTaskAssignScript() {
+
+ @Override
+ public Set calculateTaskCandidateUsers(DelegateExecution task) {
+ return singleton(11L);
+ }
+
+ @Override
+ public BpmTaskRuleScriptEnum getEnum() {
+ return BpmTaskRuleScriptEnum.LEADER_X1;
+ }
+ };
+ BpmTaskAssignScript script2 = new BpmTaskAssignScript() {
+
+ @Override
+ public Set calculateTaskCandidateUsers(DelegateExecution task) {
+ return singleton(22L);
+ }
+
+ @Override
+ public BpmTaskRuleScriptEnum getEnum() {
+ return BpmTaskRuleScriptEnum.LEADER_X2;
+ }
+ };
+ bpmCandidateScriptApiSourceInfoProcessor.setScripts(Arrays.asList(script1, script2));
+ mockGetUserMap(asSet(11L, 22L));
+
+ // 调用
+ BpmCandidateSourceInfo sourceInfo = new BpmCandidateSourceInfo();
+ sourceInfo.addRule(rule);
+ Set results = processorChain.calculateTaskCandidateUsers(null, sourceInfo);
+ // 断言
+ assertEquals(asSet(11L, 22L), results);
+ }
+
+ @Test
+ public void testRemoveDisableUsers() {
+ // 准备参数. 1L 可以找到;2L 是禁用的;3L 找不到
+ Set assigneeUserIds = asSet(1L, 2L, 3L);
+ // mock 方法
+ AdminUserRespDTO user1 = randomPojo(AdminUserRespDTO.class, o -> o.setId(1L)
+ .setStatus(CommonStatusEnum.ENABLE.getStatus()));
+ AdminUserRespDTO user2 = randomPojo(AdminUserRespDTO.class, o -> o.setId(2L)
+ .setStatus(CommonStatusEnum.DISABLE.getStatus()));
+ Map userMap = MapUtil.builder(user1.getId(), user1)
+ .put(user2.getId(), user2).build();
+ when(adminUserApi.getUserMap(eq(assigneeUserIds))).thenReturn(userMap);
+
+ // 调用
+ processorChain.removeDisableUsers(assigneeUserIds);
+ // 断言
+ assertEquals(asSet(1L), assigneeUserIds);
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/service/cc/BpmProcessInstanceCopyServiceTest.java b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/service/cc/BpmProcessInstanceCopyServiceTest.java
new file mode 100644
index 0000000..1f0ced1
--- /dev/null
+++ b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/service/cc/BpmProcessInstanceCopyServiceTest.java
@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.module.bpm.service.cc;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+import jakarta.annotation.Resource;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Import;
+
+@Import({BpmProcessInstanceCopyServiceImpl.class})
+class BpmProcessInstanceCopyServiceTest extends BaseDbUnitTest {
+ @Resource
+ private BpmProcessInstanceCopyServiceImpl service;
+
+ @Test
+ void queryById() {
+ }
+}
\ No newline at end of file
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java
index 76a4d87..22bb0b4 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/DictTypeConstants.java
@@ -13,5 +13,6 @@ public interface DictTypeConstants {
String CRM_AUDIT_STATUS = "crm_audit_status"; // CRM 审批状态
String CRM_PRODUCT_UNIT = "crm_product_unit"; // CRM 产品单位
String CRM_PRODUCT_STATUS = "crm_product_status"; // CRM 产品状态
+ String CRM_FOLLOW_UP_TYPE = "crm_follow_up_type"; // CRM 跟进方式
}
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
index a7494dc..616c868 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -14,9 +14,12 @@ public interface ErrorCodeConstants {
// ========== 线索管理 1-020-001-000 ==========
ErrorCode CLUE_NOT_EXISTS = new ErrorCode(1_020_001_000, "线索不存在");
+ ErrorCode CLUE_ANY_CLUE_NOT_EXISTS = new ErrorCode(1_020_001_001, "线索【{}】不存在");
+ ErrorCode CLUE_ANY_CLUE_ALREADY_TRANSLATED = new ErrorCode(1_020_001_002, "线索【{}】已经转化过了,请勿重复转化");
// ========== 商机管理 1-020-002-000 ==========
ErrorCode BUSINESS_NOT_EXISTS = new ErrorCode(1_020_002_000, "商机不存在");
+ ErrorCode BUSINESS_CONTRACT_EXISTS = new ErrorCode(1_020_002_001, "商机已关联合同,不能删除");
// TODO @lilleo:商机状态、商机类型,都单独错误码段
@@ -24,6 +27,7 @@ public interface ErrorCodeConstants {
// ========== 联系人管理 1-020-003-000 ==========
ErrorCode CONTACT_NOT_EXISTS = new ErrorCode(1_020_003_000, "联系人不存在");
ErrorCode CONTACT_BUSINESS_LINK_NOT_EXISTS = new ErrorCode( 1_020_003_001, "联系人商机关联不存在");
+ ErrorCode CONTACT_DELETE_FAIL_CONTRACT_LINK_EXISTS = new ErrorCode( 1_020_003_002, "联系人已关联合同,不能删除");
// ========== 回款 1-020-004-000 ==========
ErrorCode RECEIVABLE_NOT_EXISTS = new ErrorCode(1_020_004_000, "回款不存在");
@@ -39,6 +43,11 @@ public interface ErrorCodeConstants {
ErrorCode CUSTOMER_IN_POOL = new ErrorCode(1_020_006_004, "客户【{}】放入公海失败,原因:已经是公海客户");
ErrorCode CUSTOMER_LOCKED_PUT_POOL_FAIL = new ErrorCode(1_020_006_005, "客户【{}】放入公海失败,原因:客户已锁定");
ErrorCode CUSTOMER_UPDATE_OWNER_USER_FAIL = new ErrorCode(1_020_006_006, "更新客户【{}】负责人失败, 原因:系统异常");
+ ErrorCode CUSTOMER_LOCK_FAIL_IS_LOCK = new ErrorCode(1_020_006_007, "锁定客户失败,它已经处于锁定状态");
+ ErrorCode CUSTOMER_UNLOCK_FAIL_IS_UNLOCK = new ErrorCode(1_020_006_008, "解锁客户失败,它已经处于未锁定状态");
+ ErrorCode CUSTOMER_LOCK_EXCEED_LIMIT = new ErrorCode(1_020_006_009, "锁定客户失败,超出锁定规则上限");
+ ErrorCode CUSTOMER_OWNER_EXCEED_LIMIT = new ErrorCode(1_020_006_010, "操作失败,超出客户数拥有上限");
+ ErrorCode CUSTOMER_DELETE_FAIL_HAVE_REFERENCE = new ErrorCode(1_020_006_011, "删除客户失败,有关联{}");
// ========== 权限管理 1_020_007_000 ==========
ErrorCode CRM_PERMISSION_NOT_EXISTS = new ErrorCode(1_020_007_000, "数据权限不存在");
@@ -49,6 +58,7 @@ public interface ErrorCodeConstants {
ErrorCode CRM_PERMISSION_DELETE_FAIL_EXIST_OWNER = new ErrorCode(1_020_007_005, "删除数据权限失败,原因:存在负责人");
ErrorCode CRM_PERMISSION_DELETE_DENIED = new ErrorCode(1_020_007_006, "删除数据权限失败,原因:没有权限");
ErrorCode CRM_PERMISSION_DELETE_SELF_PERMISSION_FAIL_EXIST_OWNER = new ErrorCode(1_020_007_007, "删除数据权限失败,原因:不能删除负责人");
+ ErrorCode CRM_PERMISSION_CREATE_FAIL = new ErrorCode(1_020_007_008, "创建数据权限失败,原因:所加用户已有权限");
// ========== 产品 1_020_008_000 ==========
ErrorCode PRODUCT_NOT_EXISTS = new ErrorCode(1_020_008_000, "产品不存在");
@@ -72,4 +82,8 @@ public interface ErrorCodeConstants {
// ========== 客户公海规则设置 1_020_012_000 ==========
ErrorCode CUSTOMER_LIMIT_CONFIG_NOT_EXISTS = new ErrorCode(1_020_012_000, "客户限制配置不存在");
+ // ========== 跟进记录 1_020_013_000 ==========
+ ErrorCode FOLLOW_UP_RECORD_NOT_EXISTS = new ErrorCode(1_020_013_000, "跟进记录不存在");
+ ErrorCode FOLLOW_UP_RECORD_DELETE_DENIED = new ErrorCode(1_020_013_001, "删除跟进记录失败,原因:没有权限");
+
}
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/LogRecordConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/LogRecordConstants.java
new file mode 100644
index 0000000..2239947
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/LogRecordConstants.java
@@ -0,0 +1,125 @@
+package cn.iocoder.yudao.module.crm.enums;
+
+/**
+ * CRM 操作日志枚举
+ * 目的:统一管理,也减少 Service 里各种“复杂”字符串
+ *
+ * @author HUIHUI
+ */
+public interface LogRecordConstants {
+
+ // ======================= CRM_LEADS 线索 =======================
+
+ String CRM_LEADS_TYPE = "CRM 线索";
+
+ // ======================= CRM_CUSTOMER 客户 =======================
+
+ String CRM_CUSTOMER_TYPE = "CRM 客户";
+ String CRM_CUSTOMER_CREATE_SUB_TYPE = "创建客户";
+ String CRM_CUSTOMER_CREATE_SUCCESS = "创建了客户{{#customer.name}}";
+ String CRM_CUSTOMER_UPDATE_SUB_TYPE = "更新客户";
+ String CRM_CUSTOMER_UPDATE_SUCCESS = "更新了客户【{{#customerName}}】: {_DIFF{#updateReqVO}}";
+ String CRM_CUSTOMER_DELETE_SUB_TYPE = "删除客户";
+ String CRM_CUSTOMER_DELETE_SUCCESS = "删除了客户【{{#customerName}}】";
+ String CRM_CUSTOMER_TRANSFER_SUB_TYPE = "转移客户";
+ String CRM_CUSTOMER_TRANSFER_SUCCESS = "将客户【{{#customer.name}}】的负责人从【{getAdminUserById{#customer.ownerUserId}}】变更为了【{getAdminUserById{#reqVO.newOwnerUserId}}】";
+ String CRM_CUSTOMER_LOCK_SUB_TYPE = "{{#customer.lockStatus ? '解锁客户' : '锁定客户'}}";
+ String CRM_CUSTOMER_LOCK_SUCCESS = "{{#customer.lockStatus ? '将客户【' + #customer.name + '】解锁' : '将客户【' + #customer.name + '】锁定'}}";
+ String CRM_CUSTOMER_POOL_SUB_TYPE = "客户放入公海";
+ String CRM_CUSTOMER_POOL_SUCCESS = "将客户【{{#customerName}}】放入了公海";
+ String CRM_CUSTOMER_RECEIVE_SUB_TYPE = "{{#ownerUserName != null ? '分配客户' : '领取客户'}}";
+ String CRM_CUSTOMER_RECEIVE_SUCCESS = "{{#ownerUserName != null ? '将客户【' + #customer.name + '】分配给【' + #ownerUserName + '】' : '领取客户【' + #customer.name + '】'}}";
+
+ // ======================= CRM_CUSTOMER_LIMIT_CONFIG 客户限制配置 =======================
+
+ String CRM_CUSTOMER_LIMIT_CONFIG_TYPE = "CRM 客户限制配置";
+ String CRM_CUSTOMER_LIMIT_CONFIG_CREATE_SUB_TYPE = "创建客户限制配置";
+ String CRM_CUSTOMER_LIMIT_CONFIG_CREATE_SUCCESS = "创建了【{{#limitType}}】类型的客户限制配置";
+ String CRM_CUSTOMER_LIMIT_CONFIG_UPDATE_SUB_TYPE = "更新客户限制配置";
+ String CRM_CUSTOMER_LIMIT_CONFIG_UPDATE_SUCCESS = "更新了客户限制配置: {_DIFF{#updateReqVO}}";
+ String CRM_CUSTOMER_LIMIT_CONFIG_DELETE_SUB_TYPE = "删除客户限制配置";
+ String CRM_CUSTOMER_LIMIT_CONFIG_DELETE_SUCCESS = "删除了【{{#limitType}}】类型的客户限制配置";
+
+ // ======================= CRM_CUSTOMER_POOL_CONFIG 客户公海规则 =======================
+
+ String CRM_CUSTOMER_POOL_CONFIG_TYPE = "CRM 客户公海规则";
+ String CRM_CUSTOMER_POOL_CONFIG_SUB_TYPE = "{{#isPoolConfigUpdate ? '更新客户公海规则' : '创建客户公海规则'}}";
+ String CRM_CUSTOMER_POOL_CONFIG_SUCCESS = "{{#isPoolConfigUpdate ? '更新了客户公海规则' : '创建了客户公海规则'}}";
+
+ // ======================= CRM_CONTACT 联系人 =======================
+
+ String CRM_CONTACT_TYPE = "CRM 联系人";
+ String CRM_CONTACT_CREATE_SUB_TYPE = "创建联系人";
+ String CRM_CONTACT_CREATE_SUCCESS = "创建了联系人{{#contact.name}}";
+ String CRM_CONTACT_UPDATE_SUB_TYPE = "更新联系人";
+ String CRM_CONTACT_UPDATE_SUCCESS = "更新了联系人【{{#contactName}}】: {_DIFF{#updateReqVO}}";
+ String CRM_CONTACT_DELETE_SUB_TYPE = "删除联系人";
+ String CRM_CONTACT_DELETE_SUCCESS = "删除了联系人【{{#contactName}}】";
+ String CRM_CONTACT_TRANSFER_SUB_TYPE = "转移联系人";
+ String CRM_CONTACT_TRANSFER_SUCCESS = "将联系人【{{#contact.name}}】的负责人从【{getAdminUserById{#contact.ownerUserId}}】变更为了【{getAdminUserById{#reqVO.newOwnerUserId}}】";
+
+ // ======================= CRM_BUSINESS 商机 =======================
+
+ String CRM_BUSINESS_TYPE = "CRM 商机";
+ String CRM_BUSINESS_CREATE_SUB_TYPE = "创建商机";
+ String CRM_BUSINESS_CREATE_SUCCESS = "创建了商机{{#business.name}}";
+ String CRM_BUSINESS_UPDATE_SUB_TYPE = "更新商机";
+ String CRM_BUSINESS_UPDATE_SUCCESS = "更新了商机【{{#businessName}}】: {_DIFF{#updateReqVO}}";
+ String CRM_BUSINESS_DELETE_SUB_TYPE = "删除商机";
+ String CRM_BUSINESS_DELETE_SUCCESS = "删除了商机【{{#businessName}}】";
+ String CRM_BUSINESS_TRANSFER_SUB_TYPE = "转移商机";
+ String CRM_BUSINESS_TRANSFER_SUCCESS = "将商机【{{#business.name}}】的负责人从【{getAdminUserById{#business.ownerUserId}}】变更为了【{getAdminUserById{#reqVO.newOwnerUserId}}】";
+
+ // ======================= CRM_CONTRACT 合同 =======================
+
+ String CRM_CONTRACT_TYPE = "CRM 合同";
+ String CRM_CONTRACT_CREATE_SUB_TYPE = "创建合同";
+ String CRM_CONTRACT_CREATE_SUCCESS = "创建了合同{{#contract.name}}";
+ String CRM_CONTRACT_UPDATE_SUB_TYPE = "更新合同";
+ String CRM_CONTRACT_UPDATE_SUCCESS = "更新了合同【{{#contractName}}】: {_DIFF{#updateReqVO}}";
+ String CRM_CONTRACT_DELETE_SUB_TYPE = "删除合同";
+ String CRM_CONTRACT_DELETE_SUCCESS = "删除了合同【{{#contractName}}】";
+ String CRM_CONTRACT_TRANSFER_SUB_TYPE = "转移合同";
+ String CRM_CONTRACT_TRANSFER_SUCCESS = "将合同【{{#contract.name}}】的负责人从【{getAdminUserById{#contract.ownerUserId}}】变更为了【{getAdminUserById{#reqVO.newOwnerUserId}}】";
+
+ // ======================= CRM_PRODUCT 产品 =======================
+
+ String CRM_PRODUCT_TYPE = "CRM 产品";
+ String CRM_PRODUCT_CREATE_SUB_TYPE = "创建产品";
+ String CRM_PRODUCT_CREATE_SUCCESS = "创建了产品【{{#createReqVO.name}}】";
+ String CRM_PRODUCT_UPDATE_SUB_TYPE = "更新产品";
+ String CRM_PRODUCT_UPDATE_SUCCESS = "更新了产品【{{#updateReqVO.name}}】: {_DIFF{#updateReqVO}}";
+ String CRM_PRODUCT_DELETE_SUB_TYPE = "删除产品";
+ String CRM_PRODUCT_DELETE_SUCCESS = "删除了产品【{{#product.name}}】";
+
+ // ======================= CRM_PRODUCT_CATEGORY 产品分类 =======================
+
+ String CRM_PRODUCT_CATEGORY_TYPE = "CRM 产品分类";
+ String CRM_PRODUCT_CATEGORY_CREATE_SUB_TYPE = "创建产品分类";
+ String CRM_PRODUCT_CATEGORY_CREATE_SUCCESS = "创建了产品分类【{{#createReqVO.name}}】";
+ String CRM_PRODUCT_CATEGORY_UPDATE_SUB_TYPE = "更新产品分类";
+ String CRM_PRODUCT_CATEGORY_UPDATE_SUCCESS = "更新了产品分类【{{#updateReqVO.name}}】: {_DIFF{#updateReqVO}}";
+ String CRM_PRODUCT_CATEGORY_DELETE_SUB_TYPE = "删除产品分类";
+ String CRM_PRODUCT_CATEGORY_DELETE_SUCCESS = "删除了产品分类【{{#productCategory.name}}】";
+
+ // ======================= CRM_RECEIVABLE 回款 =======================
+
+ String CRM_RECEIVABLE_TYPE = "CRM 回款";
+ String CRM_RECEIVABLE_CREATE_SUB_TYPE = "创建回款";
+ String CRM_RECEIVABLE_CREATE_SUCCESS = "创建了合同【{getContractById{#receivable.contractId}}】的第【{{#receivable.period}}】期回款";
+ String CRM_RECEIVABLE_UPDATE_SUB_TYPE = "更新回款";
+ String CRM_RECEIVABLE_UPDATE_SUCCESS = "更新了合同【{getContractById{#receivable.contractId}}】的第【{{#receivable.period}}】期回款: {_DIFF{#updateReqVO}}";
+ String CRM_RECEIVABLE_DELETE_SUB_TYPE = "删除回款";
+ String CRM_RECEIVABLE_DELETE_SUCCESS = "删除了合同【{getContractById{#receivable.contractId}}】的第【{{#receivable.period}}】期回款";
+
+ // ======================= CRM_RECEIVABLE_PLAN 回款计划 =======================
+
+ String CRM_RECEIVABLE_PLAN_TYPE = "CRM 回款计划";
+ String CRM_RECEIVABLE_PLAN_CREATE_SUB_TYPE = "创建回款计划";
+ String CRM_RECEIVABLE_PLAN_CREATE_SUCCESS = "创建了合同【{getContractById{#receivablePlan.contractId}}】的第【{{#receivablePlan.period}}】期回款计划";
+ String CRM_RECEIVABLE_PLAN_UPDATE_SUB_TYPE = "更新回款计划";
+ String CRM_RECEIVABLE_PLAN_UPDATE_SUCCESS = "更新了合同【{getContractById{#receivablePlan.contractId}}】的第【{{#receivablePlan.period}}】期回款计划: {_DIFF{#updateReqVO}}";
+ String CRM_RECEIVABLE_PLAN_DELETE_SUB_TYPE = "删除回款计划";
+ String CRM_RECEIVABLE_PLAN_DELETE_SUCCESS = "删除了合同【{getContractById{#receivablePlan.contractId}}】的第【{{#receivablePlan.period}}】期回款计划";
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/business/CrmBizEndStatus.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/business/CrmBizEndStatus.java
new file mode 100644
index 0000000..55548db
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/business/CrmBizEndStatus.java
@@ -0,0 +1,55 @@
+package cn.iocoder.yudao.module.crm.enums.business;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+// TODO @lzxhqs:1)title、description、create 可以删除,非标准的 javadoc 注释哈,然后可以在类上加下这个类的注释;2)CrmBizEndStatus 改成 CrmBusinessEndStatus,非必要不缩写哈,可阅读比较重要
+/**
+ * @author lzxhqs
+ * @version 1.0
+ * @title CrmBizEndStatus
+ * @description
+ * @create 2024/1/12
+ */
+@RequiredArgsConstructor
+@Getter
+public enum CrmBizEndStatus implements IntArrayValuable {
+
+ WIN(1, "赢单"),
+ LOSE(2, "输单"),
+ INVALID(3, "无效");
+
+ public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmBizEndStatus::getStatus).toArray();
+
+ // TODO @lzxhqs:这里的方法,建议放到 49 行之后;一般类里是,静态变量,普通变量;静态方法;普通方法
+ public static boolean isWin(Integer status) {
+ return ObjectUtil.equal(WIN.getStatus(), status);
+ }
+
+ public static boolean isLose(Integer status) {
+ return ObjectUtil.equal(LOSE.getStatus(), status);
+ }
+
+ public static boolean isInvalid(Integer status) {
+ return ObjectUtil.equal(INVALID.getStatus(), status);
+ }
+
+ /**
+ * 场景类型
+ */
+ private final Integer status;
+ /**
+ * 场景名称
+ */
+ private final String name;
+
+ @Override
+ public int[] array() {
+ return ARRAYS;
+ }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/CrmBizTypeEnum.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/CrmBizTypeEnum.java
index 0a441d2..f0784ca 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/CrmBizTypeEnum.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/CrmBizTypeEnum.java
@@ -22,7 +22,9 @@ public enum CrmBizTypeEnum implements IntArrayValuable {
CRM_CONTACT(3, "联系人"),
CRM_BUSINESS(4, "商机"),
CRM_CONTRACT(5, "合同"),
- CRM_PRODUCT(6, "产品")
+ CRM_PRODUCT(6, "产品"),
+ CRM_RECEIVABLE(7, "回款"),
+ CRM_RECEIVABLE_PLAN(8, "回款计划")
;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmBizTypeEnum::getType).toArray();
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/CrmSceneEnum.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/CrmSceneTypeEnum.java
index a8d892a..945d7c6 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/CrmSceneEnum.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/common/CrmSceneTypeEnum.java
@@ -7,7 +7,6 @@ import lombok.Getter;
import java.util.Arrays;
-// TODO @puhui999:这个枚举,要不改成 CrmSceneTypeEnum
/**
* CRM 列表检索场景
*
@@ -15,14 +14,13 @@ import java.util.Arrays;
*/
@Getter
@AllArgsConstructor
-public enum CrmSceneEnum implements IntArrayValuable {
+public enum CrmSceneTypeEnum implements IntArrayValuable {
OWNER(1, "我负责的"),
- FOLLOW(2, "我关注的"),
- // TODO @puhui999:还有一个我参与的
+ INVOLVED(2, "我参与的"),
SUBORDINATE(3, "下属负责的");
- public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmSceneEnum::getType).toArray();
+ public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmSceneTypeEnum::getType).toArray();
/**
* 场景类型
@@ -37,8 +35,8 @@ public enum CrmSceneEnum implements IntArrayValuable {
return ObjUtil.equal(OWNER.getType(), type);
}
- public static boolean isFollow(Integer type) {
- return ObjUtil.equal(FOLLOW.getType(), type);
+ public static boolean isInvolved(Integer type) {
+ return ObjUtil.equal(INVOLVED.getType(), type);
}
public static boolean isSubordinate(Integer type) {
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerLimitConfigTypeEnum.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerLimitConfigTypeEnum.java
index 6300dee..2cf8d78 100644
--- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerLimitConfigTypeEnum.java
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/customer/CrmCustomerLimitConfigTypeEnum.java
@@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.crm.enums.customer;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
@@ -18,24 +20,30 @@ public enum CrmCustomerLimitConfigTypeEnum implements IntArrayValuable {
/**
* 拥有客户数限制
*/
- CUSTOMER_QUANTITY_LIMIT(1, "拥有客户数限制"),
+ CUSTOMER_OWNER_LIMIT(1, "拥有客户数限制"),
/**
* 锁定客户数限制
*/
CUSTOMER_LOCK_LIMIT(2, "锁定客户数限制"),
;
- public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmCustomerLimitConfigTypeEnum::getCode).toArray();
+ public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmCustomerLimitConfigTypeEnum::getType).toArray();
/**
* 状态
*/
- private final Integer code;
+ private final Integer type;
/**
* 状态名
*/
private final String name;
+ public static String getNameByType(Integer type) {
+ CrmCustomerLimitConfigTypeEnum typeEnum = CollUtil.findOne(CollUtil.newArrayList(CrmCustomerLimitConfigTypeEnum.values()),
+ item -> ObjUtil.equal(item.type, type));
+ return typeEnum == null ? null : typeEnum.getName();
+ }
+
@Override
public int[] array() {
return ARRAYS;
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/message/CrmContactStatusEnum.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/message/CrmContactStatusEnum.java
new file mode 100644
index 0000000..6ca5f52
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/message/CrmContactStatusEnum.java
@@ -0,0 +1,39 @@
+package cn.iocoder.yudao.module.crm.enums.message;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * CRM 联系状态
+ *
+ * @author dhb52
+ */
+@RequiredArgsConstructor
+@Getter
+public enum CrmContactStatusEnum implements IntArrayValuable {
+
+ NEEDED_TODAY(1, "今日需联系"),
+ EXPIRED(2, "已逾期"),
+ ALREADY_CONTACT(3, "已联系"),
+ ;
+
+ public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmContactStatusEnum::getType).toArray();
+
+ /**
+ * 状态
+ */
+ private final Integer type;
+ /**
+ * 状态名
+ */
+ private final String name;
+
+ @Override
+ public int[] array() {
+ return ARRAYS;
+ }
+
+}
diff --git a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/permission/CrmPermissionRoleCodeEnum.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/permission/CrmPermissionRoleCodeEnum.java
new file mode 100644
index 0000000..c9a5105
--- /dev/null
+++ b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/permission/CrmPermissionRoleCodeEnum.java
@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.crm.enums.permission;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * Crm 数据权限角色枚举
+ *
+ * @author HUIHUI
+ */
+@Getter
+@AllArgsConstructor
+public enum CrmPermissionRoleCodeEnum {
+
+ CRM_ADMIN("crm_admin", "CRM 管理员");
+
+ /**
+ * 角色标识
+ */
+ private String code;
+ /**
+ * 角色名称
+ */
+ private String name;
+
+}
+
diff --git a/yudao-module-crm/yudao-module-crm-biz/pom.xml b/yudao-module-crm/yudao-module-crm-biz/pom.xml
index 15bbc93..9e1a9e1 100644
--- a/yudao-module-crm/yudao-module-crm-biz/pom.xml
+++ b/yudao-module-crm/yudao-module-crm-biz/pom.xml
@@ -60,6 +60,10 @@
cn.iocoder.boot
yudao-spring-boot-starter-excel