# Conflicts:
#	pom.xml
#	yudao-dependencies/pom.xml
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModeImportReqVO.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelSaveReqVO.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelUpdateReqVO.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptLeaderStrategy.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptMemberStrategy.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateGroupStrategy.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidatePostStrategy.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateRoleStrategy.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserSelectStrategy.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateUserStrategy.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/message/BpmMessageService.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmActivityServiceImpl.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
#	yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/framework/permission/core/aop/CrmPermissionAspect.java
#	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApiImpl.java
#	yudao-server/pom.xml
This commit is contained in:
YunaiV 2024-10-04 17:16:23 +08:00
commit eaa22e7a56
229 changed files with 8060 additions and 1076 deletions

View File

@ -23,7 +23,8 @@
<!-- <module>yudao-module-mall</module>-->
<!-- <module>yudao-module-crm</module>-->
<!-- <module>yudao-module-erp</module>-->
<!-- AI 大模型的开启,请参考 https://doc.iocoder.cn/ai/build/ 文档,对 JDK 版本要要求! -->
<!-- <module>yudao-module-iot</module>-->
<!-- AI 大模型的开启,请参考 https://doc.iocoder.cn/ai/build/ 文档,对 JDK 版本要要求! -->
<!-- <module>yudao-module-ai</module>-->
</modules>

11
sql/mysql/bpm_update.sql Normal file
View File

@ -0,0 +1,11 @@
-- ----------------------------
-- 流程抄送表新加流程活动编号
-- ----------------------------
ALTER TABLE `pro-test`.`bpm_process_instance_copy`
ADD COLUMN `activity_id` varchar(64) NULL COMMENT '流程活动编号' AFTER `category`,
MODIFY COLUMN `task_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '任务编号' AFTER `category`;
ALTER TABLE `pro-test`.`bpm_process_definition_info`
ADD COLUMN `model_type` tinyint NOT NULL DEFAULT 10 COMMENT '流程模型的类型' AFTER `model_id`,
ADD COLUMN `simple_model` json NULL COMMENT 'SIMPLE 设计器模型数据' AFTER `form_custom_view_path`,
ADD COLUMN `visible` bit(1) NOT NULL DEFAULT 1 COMMENT '是否可见' AFTER `simple_model`;

View File

@ -66,6 +66,7 @@
<ip2region.version>2.7.0</ip2region.version>
<bizlog-sdk.version>3.0.6</bizlog-sdk.version>
<netty.version>4.1.113.Final</netty.version>
<mqtt.version>1.2.5</mqtt.version>
<!-- 三方云服务相关 -->
<okio.version>3.5.0</okio.version>
<okhttp3.version>4.11.0</okhttp3.version>
@ -626,6 +627,13 @@
</exclusions>
</dependency>
<!-- MQTT -->
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>${mqtt.version}</version>
</dependency>
<!-- 专属于 JDK8 安全漏洞升级 -->
<dependency>
<groupId>ch.qos.logback</groupId>

View File

@ -59,4 +59,11 @@ public class BeanUtils {
return new PageResult<>(list, source.getTotal());
}
public static void copyProperties(Object source, Object target) {
if (source == null || target == null) {
return;
}
BeanUtil.copyProperties(source, target, false);
}
}

View File

@ -62,6 +62,10 @@ public class BannerApplicationRunner implements ApplicationRunner {
if (isNotPresent("cn.iocoder.yudao.module.ai.framework.web.config.AiWebConfiguration")) {
System.out.println("[AI 大模型 yudao-module-ai - 已禁用][参考 https://doc.iocoder.cn/ai/build/ 开启]");
}
// IOT 物联网
if (isNotPresent("cn.iocoder.yudao.module.iot.framework.web.config.IotWebConfiguration")) {
System.out.println("[IOT 物联网 yudao-module-iot - 已禁用][参考 https://doc.iocoder.cn/iot/build/ 开启]");
}
});
}

View File

@ -376,6 +376,12 @@ public class GlobalExceptionHandler {
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
}
// 9. IOT 物联网
if (message.contains("iot_")) {
log.error("[IOT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]");
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"[IOT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]");
}
return null;
}

View File

@ -34,7 +34,12 @@ public enum AiChatRoleEnum {
### 支付宝
### 微信
除此之外不要任何解释性语句
""");
"""),
AI_KNOWLEDGE_ROLE("知识库助手", """
给你提供一些数据参考{info},请回答我的问题
请你跟进数据参考与工具返回结果回复用户的请求
""");
/**
* 角色名

View File

@ -10,4 +10,7 @@ public class AiChatConversationCreateMyReqVO {
@Schema(description = "聊天角色编号", example = "666")
private Long roleId;
@Schema(description = "知识库编号", example = "1204")
private Long knowledgeId;
}

View File

@ -21,6 +21,9 @@ public class AiChatConversationUpdateMyReqVO {
@Schema(description = "模型编号", example = "1")
private Long modelId;
@Schema(description = "知识库编号", example = "1")
private Long knowledgeId;
@Schema(description = "角色设定", example = "一个快乐的程序员")
private String systemMessage;

View File

@ -1,12 +1,12 @@
package cn.iocoder.yudao.module.ai.controller.admin.knowledge;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateMyReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeRespVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateMyReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeService;
import io.swagger.v3.oas.annotations.Operation;
@ -28,24 +28,23 @@ public class AiKnowledgeController {
@Resource
private AiKnowledgeService knowledgeService;
@GetMapping("/my-page")
@Operation(summary = "获取【我的】知识库分页")
public CommonResult<PageResult<AiKnowledgeRespVO>> getKnowledgePageMy(@Validated PageParam pageReqVO) {
PageResult<AiKnowledgeDO> pageResult = knowledgeService.getKnowledgePageMy(getLoginUserId(), pageReqVO);
@GetMapping("/page")
@Operation(summary = "获取知识库分页")
public CommonResult<PageResult<AiKnowledgeRespVO>> getKnowledgePage(@Valid AiKnowledgePageReqVO pageReqVO) {
PageResult<AiKnowledgeDO> pageResult = knowledgeService.getKnowledgePage(getLoginUserId(), pageReqVO);
return success(BeanUtils.toBean(pageResult, AiKnowledgeRespVO.class));
}
@PostMapping("/create-my")
@Operation(summary = "创建【我的】知识库")
public CommonResult<Long> createKnowledgeMy(@RequestBody @Valid AiKnowledgeCreateMyReqVO createReqVO) {
return success(knowledgeService.createKnowledgeMy(createReqVO, getLoginUserId()));
@PostMapping("/create")
@Operation(summary = "创建知识库")
public CommonResult<Long> createKnowledge(@RequestBody @Valid AiKnowledgeCreateReqVO createReqVO) {
return success(knowledgeService.createKnowledge(createReqVO, getLoginUserId()));
}
@PutMapping("/update-my")
@Operation(summary = "更新【我的】知识库")
public CommonResult<Boolean> updateKnowledgeMy(@RequestBody @Valid AiKnowledgeUpdateMyReqVO updateReqVO) {
knowledgeService.updateKnowledgeMy(updateReqVO, getLoginUserId());
@PutMapping("/update")
@Operation(summary = "更新知识库")
public CommonResult<Boolean> updateKnowledge(@RequestBody @Valid AiKnowledgeUpdateReqVO updateReqVO) {
knowledgeService.updateKnowledge(updateReqVO, getLoginUserId());
return success(true);
}
}

View File

@ -36,7 +36,7 @@ public class AiKnowledgeDocumentController {
@GetMapping("/page")
@Operation(summary = "获取文档分页")
public CommonResult<PageResult<AiKnowledgeDocumentRespVO>> getKnowledgeDocumentPageMy(@Valid AiKnowledgeDocumentPageReqVO pageReqVO) {
public CommonResult<PageResult<AiKnowledgeDocumentRespVO>> getKnowledgeDocumentPage(@Valid AiKnowledgeDocumentPageReqVO pageReqVO) {
PageResult<AiKnowledgeDocumentDO> pageResult = documentService.getKnowledgeDocumentPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, AiKnowledgeDocumentRespVO.class));
}

View File

@ -29,7 +29,7 @@ public class AiKnowledgeSegmentController {
@GetMapping("/page")
@Operation(summary = "获取段落分页")
public CommonResult<PageResult<AiKnowledgeSegmentRespVO>> getKnowledgeSegmentPageMy(@Valid AiKnowledgeSegmentPageReqVO pageReqVO) {
public CommonResult<PageResult<AiKnowledgeSegmentRespVO>> getKnowledgeSegmentPage(@Valid AiKnowledgeSegmentPageReqVO pageReqVO) {
PageResult<AiKnowledgeSegmentDO> pageResult = segmentService.getKnowledgeSegmentPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, AiKnowledgeSegmentRespVO.class));
}

View File

@ -7,9 +7,9 @@ import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - AI 知识库创建【我的】 Request VO")
@Schema(description = "管理后台 - AI 知识库创建 Request VO")
@Data
public class AiKnowledgeCreateMyReqVO {
public class AiKnowledgeCreateReqVO {
@Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ruoyi-vue-pro 用户指南")
@NotBlank(message = "知识库名称不能为空")
@ -18,11 +18,19 @@ public class AiKnowledgeCreateMyReqVO {
@Schema(description = "知识库描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "存储 ruoyi-vue-pro 操作文档")
private String description;
@Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1]")
@Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1,2,3]")
private List<Long> visibilityPermissions;
@Schema(description = "嵌入模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "嵌入模型不能为空")
private Long modelId;
@Schema(description = "相似性阈值", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.5")
@NotNull(message = "相似性阈值不能为空")
private Double similarityThreshold;
@Schema(description = "topK", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
@NotNull(message = "topK 不能为空")
private Integer topK;
}

View File

@ -23,4 +23,24 @@ public class AiKnowledgeDocumentCreateReqVO {
@URL(message = "文档 URL 格式不正确")
private String url;
@Schema(description = "每个段落的目标 token 数", requiredMode = Schema.RequiredMode.REQUIRED, example = "800")
@NotNull(message = "每个段落的目标 token 数不能为空")
private Integer defaultSegmentTokens;
@Schema(description = "每个段落的最小字符数", requiredMode = Schema.RequiredMode.REQUIRED, example = "350")
@NotNull(message = "每个段落的最小字符数不能为空")
private Integer minSegmentWordCount;
@Schema(description = "丢弃阈值:低于此阈值的段落会被丢弃", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
@NotNull(message = "丢弃阈值不能为空")
private Integer minChunkLengthToEmbed;
@Schema(description = "最大段落数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000")
@NotNull(message = "最大段落数不能为空")
private Integer maxNumSegments;
@Schema(description = "分块是否保留分隔符", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
@NotNull(message = "分块是否保留分隔符不能为空")
private Boolean keepSeparator;
}

View File

@ -0,0 +1,14 @@
package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - AI 知识库的分页 Request VO")
@Data
public class AiKnowledgePageReqVO extends PageParam {
@Schema(description = "知识库名称", example = "Java 开发手册")
private String name;
}

View File

@ -9,7 +9,7 @@ import java.util.List;
@Schema(description = "管理后台 - AI 知识库更新【我的】 Request VO")
@Data
public class AiKnowledgeUpdateMyReqVO {
public class AiKnowledgeUpdateReqVO {
@Schema(description = "对话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1204")
@NotNull(message = "知识库编号不能为空")
@ -22,7 +22,7 @@ public class AiKnowledgeUpdateMyReqVO {
@Schema(description = "知识库描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "")
private String description;
@Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1]")
@Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3")
private List<Long> visibilityPermissions;
@Schema(description = "嵌入模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")

View File

@ -0,0 +1,17 @@
package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - AI 知识库段落召回 Request VO")
@Data
public class AiKnowledgeSegmentSearchReqVO {
@Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
private Long knowledgeId;
@Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 学习路线")
private String content;
}

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.ai.dal.dataobject.chat;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
@ -64,6 +65,13 @@ public class AiChatConversationDO extends BaseDO {
*/
private Long roleId;
/**
* 知识库编号
* <p>
* 关联 {@link AiKnowledgeDO#getId()}
*/
private Long knowledgeId;
/**
* 模型编号
*

View File

@ -1,13 +1,18 @@
package cn.iocoder.yudao.module.ai.dal.dataobject.chat;
import com.baomidou.mybatisplus.annotation.TableId;
import org.springframework.ai.chat.messages.MessageType;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.*;
import org.springframework.ai.chat.messages.MessageType;
import java.util.List;
/**
* AI Chat 消息 DO
@ -66,6 +71,15 @@ public class AiChatMessageDO extends BaseDO {
*/
private Long roleId;
/**
* 段落编号数组
*
* 关联 {@link AiKnowledgeSegmentDO#getId()} 字段
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<Long> segmentIds;
/**
* 模型标志
*/

View File

@ -2,10 +2,10 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.knowledge;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.Data;
import java.util.List;
@ -38,11 +38,13 @@ public class AiKnowledgeDO extends BaseDO {
* 知识库描述
*/
private String description;
// TODO @新如果全部可见需要怎么设置
/**
* 可见权限,只能选择哪些人可见
* 可见权限,选择哪些人可见
* <p>
* -1 所有人可见其他为各自用户编号
*/
@TableField(typeHandler = JacksonTypeHandler.class)
@TableField(typeHandler = LongListTypeHandler.class)
private List<Long> visibilityPermissions;
/**
* 嵌入模型编号
@ -52,10 +54,21 @@ public class AiKnowledgeDO extends BaseDO {
* 模型标识
*/
private String model;
/**
* topK
*/
private Integer topK;
/**
* 相似度阈值
*/
private Double similarityThreshold;
/**
* 状态
* <p>
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
}

View File

@ -23,7 +23,7 @@ public class AiKnowledgeDocumentDO extends BaseDO {
private Long id;
/**
* 知识库编号
*
* <p>
* 关联 {@link AiKnowledgeDO#getId()}
*/
private Long knowledgeId;
@ -40,13 +40,39 @@ public class AiKnowledgeDocumentDO extends BaseDO {
*/
private String url;
/**
* token 数量
* 文档 token 数量
*/
private Integer tokens;
/**
* 字符数
* 文档字符数
*/
private Integer wordCount;
// ========== 自定义分段所用参数 ==========
// TODO @新3defaultChunkSizedefaultChunkSizeminChunkSizeCharsmaxNumChunks 这几个字段的命名可能要微信一起讨论下尽量命名保持风格统一哈
/**
* 每个文本块的目标 token
*/
private Integer defaultSegmentTokens;
/**
* 每个文本块的最小字符数
*/
private Integer minSegmentWordCount;
/**
* 低于此值的块会被丢弃
*/
private Integer minChunkLengthToEmbed;
/**
* 最大块数
*/
private Integer maxNumSegments;
/**
* 分块是否保留分隔符
*/
private Boolean keepSeparator;
// ===================================
/**
* 切片状态
* <p>

View File

@ -28,13 +28,13 @@ public class AiKnowledgeSegmentDO extends BaseDO {
private String vectorId;
/**
* 知识库编号
*
* <p>
* 关联 {@link AiKnowledgeDO#getId()}
*/
private Long knowledgeId;
/**
* 文档编号
*
* <p>
* 关联 {@link AiKnowledgeDocumentDO#getId()}
*/
private Long documentId;
@ -52,7 +52,7 @@ public class AiKnowledgeSegmentDO extends BaseDO {
private Integer tokens;
/**
* 状态
*
* <p>
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;

View File

@ -1,10 +1,10 @@
package cn.iocoder.yudao.module.ai.dal.mysql.knowledge;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
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.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
import org.apache.ibatis.annotations.Mapper;
@ -16,10 +16,11 @@ import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AiKnowledgeMapper extends BaseMapperX<AiKnowledgeDO> {
default PageResult<AiKnowledgeDO> selectPageByMy(Long userId, PageParam pageReqVO) {
default PageResult<AiKnowledgeDO> selectPage(Long userId, AiKnowledgePageReqVO pageReqVO) {
return selectPage(pageReqVO, new LambdaQueryWrapperX<AiKnowledgeDO>()
.eq(AiKnowledgeDO::getUserId, userId)
.eq(AiKnowledgeDO::getStatus, CommonStatusEnum.ENABLE.getStatus())
.likeIfPresent(AiKnowledgeDO::getName, pageReqVO.getName())
.and(e -> e.apply("FIND_IN_SET(" + userId + ",visibility_permissions)").or(m -> m.apply("FIND_IN_SET(-1,visibility_permissions)")))
.orderByDesc(AiKnowledgeDO::getId));
}
}

View File

@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowle
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* AI 知识库-分片 Mapper
*
@ -22,4 +24,11 @@ public interface AiKnowledgeSegmentMapper extends BaseMapperX<AiKnowledgeSegment
.likeIfPresent(AiKnowledgeSegmentDO::getContent, reqVO.getKeyword())
.orderByDesc(AiKnowledgeSegmentDO::getId));
}
default List<AiKnowledgeSegmentDO> selectListByVectorIds(List<String> vectorIdList) {
return selectList(new LambdaQueryWrapperX<AiKnowledgeSegmentDO>()
.in(AiKnowledgeSegmentDO::getVectorId, vectorIdList)
.orderByDesc(AiKnowledgeSegmentDO::getId));
}
}

View File

@ -13,6 +13,7 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatConversationMapper;
import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeService;
import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
import jakarta.annotation.Resource;
@ -22,6 +23,7 @@ import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@ -45,6 +47,8 @@ public class AiChatConversationServiceImpl implements AiChatConversationService
private AiChatModelService chatModalService;
@Resource
private AiChatRoleService chatRoleService;
@Resource
private AiKnowledgeService knowledgeService;
@Override
public Long createChatConversationMy(AiChatConversationCreateMyReqVO createReqVO, Long userId) {
@ -56,9 +60,14 @@ public class AiChatConversationServiceImpl implements AiChatConversationService
Assert.notNull(model, "必须找到默认模型");
validateChatModel(model);
// 1.3 校验知识库
if (Objects.nonNull(createReqVO.getKnowledgeId())) {
knowledgeService.validateKnowledgeExists(createReqVO.getKnowledgeId());
}
// 2. 创建 AiChatConversationDO 聊天对话
AiChatConversationDO conversation = new AiChatConversationDO().setUserId(userId).setPinned(false)
.setModelId(model.getId()).setModel(model.getModel())
.setModelId(model.getId()).setModel(model.getModel()).setKnowledgeId(createReqVO.getKnowledgeId())
.setTemperature(model.getTemperature()).setMaxTokens(model.getMaxTokens()).setMaxContexts(model.getMaxContexts());
if (role != null) {
conversation.setTitle(role.getName()).setRoleId(role.getId()).setSystemMessage(role.getSystemMessage());
@ -82,6 +91,11 @@ public class AiChatConversationServiceImpl implements AiChatConversationService
model = chatModalService.validateChatModel(updateReqVO.getModelId());
}
// 1.3 校验知识库是否存在
if (updateReqVO.getKnowledgeId() != null) {
knowledgeService.validateKnowledgeExists(updateReqVO.getKnowledgeId());
}
// 2. 更新对话信息
AiChatConversationDO updateObj = BeanUtils.toBean(updateReqVO, AiChatConversationDO.class);
if (Boolean.TRUE.equals(updateReqVO.getPinned())) {

View File

@ -12,21 +12,29 @@ import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentSearchReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper;
import cn.iocoder.yudao.module.ai.enums.AiChatRoleEnum;
import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService;
import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.messages.*;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.MessageType;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.StreamingChatModel;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Flux;
@ -59,6 +67,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
private AiChatModelService chatModalService;
@Resource
private AiApiKeyService apiKeyService;
@Resource
private AiKnowledgeSegmentService knowledgeSegmentService;
@Transactional(rollbackFor = Exception.class)
public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) {
@ -80,13 +90,16 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model,
userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext());
// 3.2 创建 chat 需要的 Prompt
Prompt prompt = buildPrompt(conversation, historyMessages, model, sendReqVO);
// 3.2 召回段落
List<AiKnowledgeSegmentDO> segmentList = recallSegment(sendReqVO.getContent(), conversation.getKnowledgeId());
// 3.3 创建 chat 需要的 Prompt
Prompt prompt = buildPrompt(conversation, historyMessages, segmentList, model, sendReqVO);
ChatResponse chatResponse = chatModel.call(prompt);
// 3.3 段式返回
// 3.4 段式返回
String newContent = chatResponse.getResult().getOutput().getContent();
chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(newContent));
chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setSegmentIds(convertList(segmentList, AiKnowledgeSegmentDO::getId)).setContent(newContent));
return new AiChatMessageSendRespVO().setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class))
.setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class).setContent(newContent));
}
@ -111,11 +124,15 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model,
userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext());
// 3.2 构建 Prompt并进行调用
Prompt prompt = buildPrompt(conversation, historyMessages, model, sendReqVO);
// 3.2 召回段落
List<AiKnowledgeSegmentDO> segmentList = recallSegment(sendReqVO.getContent(), conversation.getKnowledgeId());
// 3.3 构建 Prompt并进行调用
Prompt prompt = buildPrompt(conversation, historyMessages, segmentList, model, sendReqVO);
Flux<ChatResponse> streamResponse = chatModel.stream(prompt);
// 3.3 流式返回
// 3.4 流式返回
// TODO 注意Schedulers.immediate() 目的是避免默认 Schedulers.parallel() 并发消费 chunk 导致 SSE 响应前端会乱序问题
StringBuffer contentBuffer = new StringBuffer();
return streamResponse.map(chunk -> {
@ -128,7 +145,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
}).doOnComplete(() -> {
// 忽略租户因为 Flux 异步无法透传租户
TenantUtils.executeIgnore(() ->
chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(contentBuffer.toString())));
chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setSegmentIds(convertList(segmentList, AiKnowledgeSegmentDO::getId))
.setContent(contentBuffer.toString())));
}).doOnError(throwable -> {
log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable);
// 忽略租户因为 Flux 异步无法透传租户
@ -137,18 +155,35 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
}).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR)));
}
private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages,
private List<AiKnowledgeSegmentDO> recallSegment(String content, Long knowledgeId) {
if (Objects.isNull(knowledgeId)) {
return Collections.emptyList();
}
return knowledgeSegmentService.similaritySearch(new AiKnowledgeSegmentSearchReqVO().setKnowledgeId(knowledgeId).setContent(content));
}
private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages,List<AiKnowledgeSegmentDO> segmentList,
AiChatModelDO model, AiChatMessageSendReqVO sendReqVO) {
// 1. 构建 Prompt Message 列表
List<Message> chatMessages = new ArrayList<>();
// 1.1 system context 角色设定
// 1.1 召回内容消息构建
if (CollUtil.isNotEmpty(segmentList)) {
PromptTemplate promptTemplate = new PromptTemplate(AiChatRoleEnum.AI_KNOWLEDGE_ROLE.getSystemMessage());
StringBuilder infoBuilder = StrUtil.builder();
segmentList.forEach(segment -> infoBuilder.append(System.lineSeparator()).append(segment.getContent()));
Message message = promptTemplate.createMessage(Map.of("info", infoBuilder.toString()));
chatMessages.add(message);
}
// 1.2 system context 角色设定
if (StrUtil.isNotBlank(conversation.getSystemMessage())) {
chatMessages.add(new SystemMessage(conversation.getSystemMessage()));
}
// 1.2 history message 历史消息
// 1.3 history message 历史消息
List<AiChatMessageDO> contextMessages = filterContextMessages(messages, conversation, sendReqVO);
contextMessages.forEach(message -> chatMessages.add(AiUtils.buildMessage(message.getType(), message.getContent())));
// 1.3 user message 新发送消息
// 1.4 user message 新发送消息
chatMessages.add(new UserMessage(sendReqVO.getContent()));
// 2. 构建 ChatOptions 对象
@ -160,12 +195,12 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
/**
* 从历史消息中获得倒序的 n 组消息作为消息上下文
*
* <p>
* n 指的是 user + assistant 形成一组
*
* @param messages 消息列表
* @param messages 消息列表
* @param conversation 对话
* @param sendReqVO 发送请求
* @param sendReqVO 发送请求
* @return 消息上下文
*/
private List<AiChatMessageDO> filterContextMessages(List<AiChatMessageDO> messages,
@ -182,7 +217,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
}
AiChatMessageDO userMessage = CollUtil.get(messages, i - 1);
if (userMessage == null || ObjUtil.notEqual(assistantMessage.getReplyId(), userMessage.getId())
|| StrUtil.isEmpty(assistantMessage.getContent())) {
|| StrUtil.isEmpty(assistantMessage.getContent())) {
continue;
}
// 由于后续要 reverse 反转所以先添加 assistantMessage

View File

@ -9,15 +9,11 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeDocumentMapper;
import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper;
import cn.iocoder.yudao.module.ai.enums.knowledge.AiKnowledgeDocumentStatusEnum;
import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
@ -48,24 +44,16 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic
@Resource
private AiKnowledgeSegmentMapper segmentMapper;
@Resource
private TokenTextSplitter tokenTextSplitter;
@Resource
private TokenCountEstimator tokenCountEstimator;
@Resource
private AiApiKeyService apiKeyService;
@Resource
private AiKnowledgeService knowledgeService;
@Resource
private AiChatModelService chatModelService;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createKnowledgeDocument(AiKnowledgeDocumentCreateReqVO createReqVO) {
// 0. 校验
AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(createReqVO.getKnowledgeId());
AiChatModelDO model = chatModelService.validateChatModel(knowledge.getModelId());
// 0. 校验并获取向量存储实例
VectorStore vectorStore = knowledgeService.getVectorStoreById(createReqVO.getKnowledgeId());
// 1.1 下载文档
TikaDocumentReader loader = new TikaDocumentReader(downloadFile(createReqVO.getUrl()));
@ -82,6 +70,9 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic
return documentId;
}
// 2 构造文本分段器
TokenTextSplitter tokenTextSplitter = new TokenTextSplitter(createReqVO.getDefaultSegmentTokens(), createReqVO.getMinSegmentWordCount(), createReqVO.getMinChunkLengthToEmbed(),
createReqVO.getMaxNumSegments(), createReqVO.getKeepSeparator());
// 2.1 文档分段
List<Document> segments = tokenTextSplitter.apply(documents);
// 2.2 分段内容入库
@ -92,9 +83,7 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic
.setStatus(CommonStatusEnum.ENABLE.getStatus()));
segmentMapper.insertBatch(segmentDOList);
// 3.1 获取向量存储实例
VectorStore vectorStore = apiKeyService.getOrCreateVectorStore(model.getKeyId());
// 3.2 向量化并存储
// 3. 向量化并存储
segments.forEach(segment -> segment.getMetadata().put(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, createReqVO.getKnowledgeId()));
vectorStore.add(segments);
return documentId;

View File

@ -2,10 +2,13 @@ package cn.iocoder.yudao.module.ai.service.knowledge;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentSearchReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
import java.util.List;
/**
* AI 知识库段落 Service 接口
*
@ -35,4 +38,12 @@ public interface AiKnowledgeSegmentService {
*/
void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO);
/**
* 召回段落
*
* @param reqVO 召回请求信息
* @return 召回的段落
*/
List<AiKnowledgeSegmentDO> similaritySearch(AiKnowledgeSegmentSearchReqVO reqVO);
}

View File

@ -1,16 +1,34 @@
package cn.iocoder.yudao.module.ai.service.knowledge;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentSearchReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper;
import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_NOT_EXISTS;
/**
* AI 知识库分片 Service 实现类
*
@ -23,6 +41,13 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
@Resource
private AiKnowledgeSegmentMapper segmentMapper;
@Resource
private AiKnowledgeService knowledgeService;
@Resource
private AiChatModelService chatModelService;
@Resource
private AiApiKeyService apiKeyService;
@Override
public PageResult<AiKnowledgeSegmentDO> getKnowledgeSegmentPage(AiKnowledgeSegmentPageReqVO pageReqVO) {
return segmentMapper.selectPage(pageReqVO);
@ -30,13 +55,80 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
@Override
public void updateKnowledgeSegment(AiKnowledgeSegmentUpdateReqVO reqVO) {
segmentMapper.updateById(BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class));
// TODO @xin 重新向量化
// 1. 校验
AiKnowledgeSegmentDO oldKnowledgeSegment = validateKnowledgeSegmentExists(reqVO.getId());
// 2.1 获取知识库向量实例
VectorStore vectorStore = knowledgeService.getVectorStoreById(oldKnowledgeSegment.getKnowledgeId());
// 2.2 删除原向量
vectorStore.delete(List.of(oldKnowledgeSegment.getVectorId()));
// 2.3 重新向量化
Document document = new Document(reqVO.getContent());
document.getMetadata().put(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, oldKnowledgeSegment.getKnowledgeId());
vectorStore.add(List.of(document));
// 3. 更新段落内容
AiKnowledgeSegmentDO knowledgeSegment = BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class);
knowledgeSegment.setVectorId(document.getId());
segmentMapper.updateById(knowledgeSegment);
}
@Override
public void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO) {
segmentMapper.updateById(BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class));
// TODO @xin 1.禁用删除向量 2.启用重新向量化
// 0 校验
AiKnowledgeSegmentDO oldKnowledgeSegment = validateKnowledgeSegmentExists(reqVO.getId());
// 1 获取知识库向量实例
VectorStore vectorStore = knowledgeService.getVectorStoreById(oldKnowledgeSegment.getKnowledgeId());
AiKnowledgeSegmentDO knowledgeSegment = BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class);
if (Objects.equals(reqVO.getStatus(), CommonStatusEnum.ENABLE.getStatus())) {
// 2.1 启用重新向量化
Document document = new Document(oldKnowledgeSegment.getContent());
document.getMetadata().put(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, oldKnowledgeSegment.getKnowledgeId());
vectorStore.add(List.of(document));
knowledgeSegment.setVectorId(document.getId());
} else {
// 2.2 禁用删除向量
vectorStore.delete(List.of(oldKnowledgeSegment.getVectorId()));
knowledgeSegment.setVectorId("");
}
// 3 更新段落状态
segmentMapper.updateById(knowledgeSegment);
}
@Override
public List<AiKnowledgeSegmentDO> similaritySearch(AiKnowledgeSegmentSearchReqVO reqVO) {
// 1. 校验
AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(reqVO.getKnowledgeId());
AiChatModelDO model = chatModelService.validateChatModel(knowledge.getModelId());
// 2. 获取向量存储实例
VectorStore vectorStore = apiKeyService.getOrCreateVectorStore(model.getKeyId());
// 3.1 向量检索
List<Document> documentList = vectorStore.similaritySearch(SearchRequest.query(reqVO.getContent())
.withTopK(knowledge.getTopK())
.withSimilarityThreshold(knowledge.getSimilarityThreshold())
.withFilterExpression(new FilterExpressionBuilder().eq(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, reqVO.getKnowledgeId()).build()));
if (CollUtil.isEmpty(documentList)) {
return ListUtil.empty();
}
// 3.2 段落召回
return segmentMapper.selectListByVectorIds(CollUtil.getFieldValues(documentList, "id", String.class));
}
/**
* 校验段落是否存在
*
* @param id 文档编号
* @return 段落信息
*/
private AiKnowledgeSegmentDO validateKnowledgeSegmentExists(Long id) {
AiKnowledgeSegmentDO knowledgeSegment = segmentMapper.selectById(id);
if (knowledgeSegment == null) {
throw exception(KNOWLEDGE_SEGMENT_NOT_EXISTS);
}
return knowledgeSegment;
}
}

View File

@ -1,10 +1,11 @@
package cn.iocoder.yudao.module.ai.service.knowledge;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateMyReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateMyReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
import org.springframework.ai.vectorstore.VectorStore;
/**
* AI 知识库-基础信息 Service 接口
@ -14,23 +15,21 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
public interface AiKnowledgeService {
/**
* 创建我的知识库
* 创建知识库
*
* @param createReqVO 创建信息
* @param userId 用户编号
* @return 编号
*/
Long createKnowledgeMy(AiKnowledgeCreateMyReqVO createReqVO, Long userId);
Long createKnowledge(AiKnowledgeCreateReqVO createReqVO, Long userId);
/**
* 创建我的知识库
* 更新知识库
*
* @param updateReqVO 更新信息
* @param userId 用户编号
*/
void updateKnowledgeMy(AiKnowledgeUpdateMyReqVO updateReqVO, Long userId);
void updateKnowledge(AiKnowledgeUpdateReqVO updateReqVO, Long userId);
/**
* 校验知识库是否存在
@ -40,11 +39,20 @@ public interface AiKnowledgeService {
AiKnowledgeDO validateKnowledgeExists(Long id);
/**
* 获得我的知识库分页
* 获得知识库分页
*
* @param userId 用户编号
* @param pageReqVO 分页查询
* @param userId 用户编号
* @param pageReqVO 分页查询
* @return 知识库分页
*/
PageResult<AiKnowledgeDO> getKnowledgePageMy(Long userId, PageParam pageReqVO);
PageResult<AiKnowledgeDO> getKnowledgePage(Long userId, AiKnowledgePageReqVO pageReqVO);
/**
* 根据知识库编号获取向量存储实例
*
* @param id 知识库编号
* @return 向量存储实例
*/
VectorStore getVectorStoreById(Long id);
}

View File

@ -2,17 +2,19 @@ package cn.iocoder.yudao.module.ai.service.knowledge;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateMyReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateMyReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeMapper;
import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@ -27,16 +29,18 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_NOT_
@Slf4j
public class AiKnowledgeServiceImpl implements AiKnowledgeService {
@Resource
private AiChatModelService chatModalService;
@Resource
private AiKnowledgeMapper knowledgeMapper;
@Resource
private AiChatModelService chatModelService;
@Resource
private AiApiKeyService apiKeyService;
@Override
public Long createKnowledgeMy(AiKnowledgeCreateMyReqVO createReqVO, Long userId) {
public Long createKnowledge(AiKnowledgeCreateReqVO createReqVO, Long userId) {
// 1. 校验模型配置
AiChatModelDO model = chatModalService.validateChatModel(createReqVO.getModelId());
AiChatModelDO model = chatModelService.validateChatModel(createReqVO.getModelId());
// 2. 插入知识库
AiKnowledgeDO knowledgeBase = BeanUtils.toBean(createReqVO, AiKnowledgeDO.class)
@ -46,14 +50,14 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
}
@Override
public void updateKnowledgeMy(AiKnowledgeUpdateMyReqVO updateReqVO, Long userId) {
public void updateKnowledge(AiKnowledgeUpdateReqVO updateReqVO, Long userId) {
// 1.1 校验知识库存在
AiKnowledgeDO knowledgeBaseDO = validateKnowledgeExists(updateReqVO.getId());
if (ObjUtil.notEqual(knowledgeBaseDO.getUserId(), userId)) {
throw exception(KNOWLEDGE_NOT_EXISTS);
}
// 1.2 校验模型配置
AiChatModelDO model = chatModalService.validateChatModel(updateReqVO.getModelId());
AiChatModelDO model = chatModelService.validateChatModel(updateReqVO.getModelId());
// 2. 更新知识库
AiKnowledgeDO updateDO = BeanUtils.toBean(updateReqVO, AiKnowledgeDO.class);
@ -71,8 +75,16 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
}
@Override
public PageResult<AiKnowledgeDO> getKnowledgePageMy(Long userId, PageParam pageReqVO) {
return knowledgeMapper.selectPageByMy(userId, pageReqVO);
public PageResult<AiKnowledgeDO> getKnowledgePage(Long userId, AiKnowledgePageReqVO pageReqVO) {
return knowledgeMapper.selectPage(userId, pageReqVO);
}
@Override
public VectorStore getVectorStoreById(Long id) {
AiKnowledgeDO knowledge = validateKnowledgeExists(id);
AiChatModelDO model = chatModelService.validateChatModel(knowledge.getModelId());
// 创建或获取 VectorStore 对象
return apiKeyService.getOrCreateVectorStore(model.getKeyId());
}
}

View File

@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.ai.service.model;
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory;
import cn.iocoder.yudao.framework.ai.core.factory.AiVectorStoreFactory;
import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
@ -39,8 +38,6 @@ public class AiApiKeyServiceImpl implements AiApiKeyService {
@Resource
private AiModelFactory modelFactory;
@Resource
private AiVectorStoreFactory vectorFactory;
@Override
public Long createApiKey(AiApiKeySaveReqVO createReqVO) {
@ -149,7 +146,8 @@ public class AiApiKeyServiceImpl implements AiApiKeyService {
public VectorStore getOrCreateVectorStore(Long id) {
AiApiKeyDO apiKey = validateApiKey(id);
AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform());
return vectorFactory.getOrCreateVectorStore(getEmbeddingModel(id), platform, apiKey.getApiKey(), apiKey.getUrl());
// 创建或获取 VectorStore 对象
return modelFactory.getOrCreateVectorStore(getEmbeddingModel(id), platform, apiKey.getApiKey(), apiKey.getUrl());
}
}

View File

@ -46,11 +46,13 @@
</dependency>
<!-- 向量化,基于 Redis 存储Tika 解析内容 -->
<dependency>
<groupId>${spring-ai.groupId}</groupId>
<artifactId>spring-ai-transformers-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!-- 暂不做经济型,先注释 TODO 经济型是啥呀? -->
<!-- <dependency>-->
<!-- <groupId>${spring-ai.groupId}</groupId>-->
<!-- <artifactId>spring-ai-transformers-spring-boot-starter</artifactId>-->
<!-- <version>${spring-ai.version}</version>-->
<!-- </dependency>-->
<dependency>
<groupId>${spring-ai.groupId}</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>

View File

@ -2,8 +2,6 @@ package cn.iocoder.yudao.framework.ai.config;
import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory;
import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactoryImpl;
import cn.iocoder.yudao.framework.ai.core.factory.AiVectorStoreFactory;
import cn.iocoder.yudao.framework.ai.core.factory.AiVectorStoreFactoryImpl;
import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel;
import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions;
import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
@ -38,11 +36,6 @@ public class YudaoAiAutoConfiguration {
return new AiModelFactoryImpl();
}
@Bean
public AiVectorStoreFactory aiVectorFactory() {
return new AiVectorStoreFactoryImpl();
}
// ========== 各种 AI Client 创建 ==========
@ -89,7 +82,7 @@ public class YudaoAiAutoConfiguration {
// TODO @xin 免费版本
// @Bean
// @Lazy // TODO 芋艿临时注释避免无法启动
// public EmbeddingModel transformersEmbeddingClient() {
// public TransformersEmbeddingModel transformersEmbeddingClient() {
// return new TransformersEmbeddingModel(MetadataMode.EMBED);
// }
@ -98,23 +91,24 @@ public class YudaoAiAutoConfiguration {
*/
// @Bean
// @Lazy // TODO 芋艿临时注释避免无法启动
// public RedisVectorStore vectorStore(TongYiTextEmbeddingModel tongYiTextEmbeddingModel, RedisVectorStoreProperties properties,
// public RedisVectorStore vectorStore(TransformersEmbeddingModel embeddingModel, RedisVectorStoreProperties properties,
// RedisProperties redisProperties) {
// var config = RedisVectorStore.RedisVectorStoreConfig.builder()
// .withIndexName(properties.getIndex())
// .withPrefix(properties.getPrefix())
// .withMetadataFields(new RedisVectorStore.MetadataField("knowledgeId", Schema.FieldType.NUMERIC))
// .build();
//
// RedisVectorStore redisVectorStore = new RedisVectorStore(config, tongYiTextEmbeddingModel,
// RedisVectorStore redisVectorStore = new RedisVectorStore(config, embeddingModel,
// new JedisPooled(redisProperties.getHost(), redisProperties.getPort()),
// properties.isInitializeSchema());
// redisVectorStore.afterPropertiesSet();
// return redisVectorStore;
// }
@Bean
@Lazy // TODO 芋艿临时注释避免无法启动
public TokenTextSplitter tokenTextSplitter() {
//TODO @xin 配置提取
return new TokenTextSplitter(500, 100, 5, 10000, true);
}

View File

@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.image.ImageModel;
import org.springframework.ai.vectorstore.VectorStore;
/**
* AI Model 模型工厂的接口类
@ -92,4 +93,17 @@ public interface AiModelFactory {
*/
EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url);
/**
* 基于指定配置获得 VectorStore 对象
* <p>
* 如果不存在则进行创建
*
* @param embeddingModel 嵌入模型
* @param platform 平台
* @param apiKey API KEY
* @param url API URL
* @return VectorStore 对象
*/
VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel, AiPlatformEnum platform, String apiKey, String url);
}

View File

@ -13,6 +13,7 @@ import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel;
import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
import cn.iocoder.yudao.framework.common.util.spring.SpringUtils;
import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration;
import com.alibaba.cloud.ai.tongyi.TongYiConnectionProperties;
import com.alibaba.cloud.ai.tongyi.chat.TongYiChatModel;
@ -54,13 +55,18 @@ import org.springframework.ai.qianfan.api.QianFanApi;
import org.springframework.ai.qianfan.api.QianFanImageApi;
import org.springframework.ai.stabilityai.StabilityAiImageModel;
import org.springframework.ai.stabilityai.api.StabilityAiApi;
import org.springframework.ai.vectorstore.RedisVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.zhipuai.ZhiPuAiChatModel;
import org.springframework.ai.zhipuai.ZhiPuAiImageModel;
import org.springframework.ai.zhipuai.api.ZhiPuAiApi;
import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClient;
import redis.clients.jedis.JedisPooled;
import redis.clients.jedis.search.Schema;
import java.util.List;
@ -191,6 +197,25 @@ public class AiModelFactoryImpl implements AiModelFactory {
});
}
@Override
public VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel, AiPlatformEnum platform, String apiKey, String url) {
String cacheKey = buildClientCacheKey(VectorStore.class, platform, apiKey, url);
return Singleton.get(cacheKey, (Func0<VectorStore>) () -> {
String prefix = StrUtil.format("{}#{}:", platform.getPlatform(), apiKey);
var config = RedisVectorStore.RedisVectorStoreConfig.builder()
.withIndexName(cacheKey)
.withPrefix(prefix)
.withMetadataFields(new RedisVectorStore.MetadataField("knowledgeId", Schema.FieldType.NUMERIC))
.build();
RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class);
RedisVectorStore redisVectorStore = new RedisVectorStore(config, embeddingModel,
new JedisPooled(redisProperties.getHost(), redisProperties.getPort()),
true);
redisVectorStore.afterPropertiesSet();
return redisVectorStore;
});
}
private static String buildClientCacheKey(Class<?> clazz, Object... params) {
if (ArrayUtil.isEmpty(params)) {
return clazz.getName();

View File

@ -1,28 +0,0 @@
package cn.iocoder.yudao.framework.ai.core.factory;
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.VectorStore;
// TODO @xin也放到 AiModelFactory 里面好了后续改成 AiFactory
/**
* AI Vector 模型工厂的接口类
*
* @author xiaoxin
*/
public interface AiVectorStoreFactory {
/**
* 基于指定配置获得 VectorStore 对象
* <p>
* 如果不存在则进行创建
*
* @param embeddingModel 嵌入模型
* @param platform 平台
* @param apiKey API KEY
* @param url API URL
* @return VectorStore 对象
*/
VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel, AiPlatformEnum platform, String apiKey, String url);
}

View File

@ -1,52 +0,0 @@
package cn.iocoder.yudao.framework.ai.core.factory;
import cn.hutool.core.lang.Singleton;
import cn.hutool.core.lang.func.Func0;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
import cn.iocoder.yudao.framework.common.util.spring.SpringUtils;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.RedisVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import redis.clients.jedis.JedisPooled;
/**
* AI Vector 模型工厂的实现类
* 使用 redisVectorStore 实现 VectorStore
*
* @author xiaoxin
*/
public class AiVectorStoreFactoryImpl implements AiVectorStoreFactory {
@Override
public VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel, AiPlatformEnum platform, String apiKey, String url) {
String cacheKey = buildClientCacheKey(VectorStore.class, platform, apiKey, url);
return Singleton.get(cacheKey, (Func0<VectorStore>) () -> {
// TODO 芋艿 @xin 这两个配置取哪好呢
// TODO 不同模型的向量维度可能会不一样目前看貌似是以 index 来做区分的维度不一样存不到一个 index
// TODO 回复好的哈
String index = "default-index";
String prefix = "default:";
var config = RedisVectorStore.RedisVectorStoreConfig.builder()
.withIndexName(index)
.withPrefix(prefix)
.build();
RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class);
RedisVectorStore redisVectorStore = new RedisVectorStore(config, embeddingModel,
new JedisPooled(redisProperties.getHost(), redisProperties.getPort()),
true);
redisVectorStore.afterPropertiesSet();
return redisVectorStore;
});
}
private static String buildClientCacheKey(Class<?> clazz, Object... params) {
if (ArrayUtil.isEmpty(params)) {
return clazz.getName();
}
return StrUtil.format("{}#{}", clazz.getName(), ArrayUtil.join(params, "_"));
}
}

View File

@ -23,6 +23,7 @@ public interface ErrorCodeConstants {
"原因:用户任务({})未配置审批人,请点击【流程设计】按钮,选择该它的【任务(审批人)】进行配置");
ErrorCode MODEL_DEPLOY_FAIL_BPMN_START_EVENT_NOT_EXISTS = new ErrorCode(1_009_002_005, "部署流程失败原因BPMN 流程图中,没有开始事件");
ErrorCode MODEL_DEPLOY_FAIL_BPMN_USER_TASK_NAME_NOT_EXISTS = new ErrorCode(1_009_002_006, "部署流程失败原因BPMN 流程图中,用户任务({})的名字不存在");
ErrorCode MODEL_UPDATE_FAIL_NOT_MANAGER = new ErrorCode(1_009_002_007, "操作流程失败,原因:你不是该流程的管理员");
// ========== 流程定义 1-009-003-000 ==========
ErrorCode PROCESS_DEFINITION_KEY_NOT_MATCH = new ErrorCode(1_009_003_000, "流程定义的标识期望是({}),当前是({}),请修改 BPMN 流程图");
@ -36,6 +37,7 @@ public interface ErrorCodeConstants {
ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF = new ErrorCode(1_009_004_002, "流程取消失败,该流程不是你发起的");
ErrorCode PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG = new ErrorCode(1_009_004_003, "审批任务({})的审批人未配置");
ErrorCode PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_EXISTS = new ErrorCode(1_009_004_004, "审批任务({})的审批人({})不存在");
ErrorCode PROCESS_INSTANCE_START_USER_CAN_START = new ErrorCode(1_009_004_005, "发起流程失败,你没有权限发起该流程");
// ========== 流程任务 1-009-005-000 ==========
ErrorCode TASK_OPERATE_FAIL_ASSIGN_NOT_SELF = new ErrorCode(1_009_005_001, "操作失败,原因:该任务的审批人不是你");

View File

@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import cn.hutool.core.util.ArrayUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* BPM 边界事件 (boundary event) 自定义类型枚举
*
* @author jason
*/
@Getter
@AllArgsConstructor
public enum BpmBoundaryEventType {
USER_TASK_TIMEOUT(1,"用户任务超时");
private final Integer type;
private final String name;
public static BpmBoundaryEventType typeOf(Integer type) {
return ArrayUtil.firstMatch(eventType -> eventType.getType().equals(type), values());
}
}

View File

@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import cn.hutool.core.util.ArrayUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* BPM 表单权限的枚举
*
* @author jason
*/
@Getter
@AllArgsConstructor
public enum BpmFieldPermissionEnum {
READ(1, "只读"),
WRITE(2, "可编辑"),
NONE(3, "隐藏");
/**
* 权限
*/
private final Integer permission;
/**
* 名字
*/
private final String name;
public static BpmFieldPermissionEnum valueOf(Integer permission) {
return ArrayUtil.firstMatch(item -> item.getPermission().equals(permission), values());
}
}

View File

@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* BPM 模型的类型的枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum BpmModelTypeEnum implements IntArrayValuable {
BPMN(10, "BPMN 设计器"), // https://bpmn.io/toolkit/bpmn-js/
SIMPLE(20, "SIMPLE 设计器"); // 参考钉钉飞书工作流的设计器
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmModelTypeEnum::getType).toArray();
private final Integer type;
private final String name;
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 仿钉钉的流程器设计器条件节点的条件类型
*
* @author jason
*/
@Getter
@AllArgsConstructor
public enum BpmSimpleModeConditionType implements IntArrayValuable {
EXPRESSION(1, "条件表达式"),
RULE(2, "条件规则");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmSimpleModeConditionType::getType).toArray();
private final Integer type;
private final String name;
public static BpmSimpleModeConditionType valueOf(Integer type) {
return ArrayUtil.firstMatch(nodeType -> nodeType.getType().equals(type), values());
}
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,76 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Objects;
/**
* 仿钉钉的流程器设计器的模型节点类型
*
* @author jason
*/
@Getter
@AllArgsConstructor
public enum BpmSimpleModelNodeType implements IntArrayValuable {
// 0 ~ 1 开始和结束
START_NODE(0, "startEvent", "开始节点"),
END_NODE(1, "endEvent", "结束节点"),
// 10 ~ 49 各种节点
START_USER_NODE(10, "userTask", "发起人节点"), // 发起人节点前端的开始节点Id 固定
APPROVE_NODE(11, "userTask", "审批人节点"),
COPY_NODE(12, "serviceTask", "抄送人节点"),
// 50 ~ 条件分支
CONDITION_NODE(50, "sequenceFlow", "条件节点"), // 用于构建流转条件的表达式
CONDITION_BRANCH_NODE(51, " “parallelGateway”", "条件分支节点"), // TODO @jason是不是改成叫 条件分支
PARALLEL_BRANCH_NODE(52, "exclusiveGateway", "并行分支节点"), // TODO @jason是不是一个 并行分支 就可以啦 后面是否去掉并行网关只用包容网关
INCLUSIVE_BRANCH_NODE(53, "inclusiveGateway", "包容分支节点"),
// TODO @jason建议整合 join最终只有 条件分支并行分支包容分支三种~
// TODO @芋艿 感觉还是分开好理解一点,也好处理一点前端结构中把聚合节点显示并传过来
;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmSimpleModelNodeType::getType).toArray();
public static final String BPMN_USER_TASK_TYPE = "userTask";
private final Integer type;
private final String bpmnType;
private final String name;
/**
* 判断是否为分支节点
*
* @param type 节点类型
*/
public static boolean isBranchNode(Integer type) {
return Objects.equals(CONDITION_BRANCH_NODE.getType(), type)
|| Objects.equals(PARALLEL_BRANCH_NODE.getType(), type)
|| Objects.equals(INCLUSIVE_BRANCH_NODE.getType(), type);
}
/**
* 判断是否需要记录的节点
*
* @param bpmnType bpmn节点类型
*/
public static boolean isRecordNode(String bpmnType) {
return Objects.equals(APPROVE_NODE.getBpmnType(), bpmnType)
|| Objects.equals(END_NODE.getBpmnType(), bpmnType);
}
public static BpmSimpleModelNodeType valueOf(Integer type) {
return ArrayUtil.firstMatch(nodeType -> nodeType.getType().equals(type), values());
}
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* BPM 多人审批方式的枚举
*
* @author jason
*/
@Getter
@AllArgsConstructor
public enum BpmUserTaskApproveMethodEnum implements IntArrayValuable {
RANDOM(1, "随机挑选一人审批"),
RATIO(2, "多人会签(按通过比例)"), // 会签按通过比例
ANY(3, "多人或签(一人通过或拒绝)"), // 或签通过只需一人拒绝只需一人
SEQUENTIAL(4, "依次审批"); // 依次审批
/**
* 审批方式
*/
private final Integer method;
/**
* 名字
*/
private final String name;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmUserTaskApproveMethodEnum::getMethod).toArray();
public static BpmUserTaskApproveMethodEnum valueOf(Integer method) {
return ArrayUtil.firstMatch(item -> item.getMethod().equals(method), values());
}
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 用户任务的审批类型枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum BpmUserTaskApproveTypeEnum implements IntArrayValuable {
USER(1), // 人工审批
AUTO_APPROVE(2), // 自动通过
AUTO_REJECT(3); // 自动拒绝
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmUserTaskApproveTypeEnum::getType).toArray();
private final Integer type;
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
* BPM 用户任务的审批人为空时处理类型枚举
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Getter
public enum BpmUserTaskAssignEmptyHandlerTypeEnum implements IntArrayValuable {
APPROVE(1), // 自动通过
REJECT(2), // 自动拒绝
ASSIGN_USER(3), // 指定人员审批
ASSIGN_ADMIN(4), // 转交给流程管理员
;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmUserTaskAssignEmptyHandlerTypeEnum::getType).toArray();
private final Integer type;
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
* BPM 用户任务的审批人与发起人相同时处理类型枚举
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Getter
public enum BpmUserTaskAssignStartUserHandlerTypeEnum implements IntArrayValuable {
START_USER_AUDIT(1), // 由发起人对自己审批
SKIP(2), // 自动跳过参考飞书1如果当前节点还有其他审批人则交由其他审批人进行审批2如果当前节点没有其他审批人则该节点自动通过
TRANSFER_DEPT_LEADER(3); // 转交给部门负责人审批参考飞书若部门负责人为空则自动通过
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmUserTaskAssignStartUserHandlerTypeEnum::getType).toArray();
private final Integer type;
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* BPM 用户任务拒绝处理类型枚举
*
* @author jason
*/
@Getter
@AllArgsConstructor
public enum BpmUserTaskRejectHandlerType implements IntArrayValuable {
FINISH_PROCESS_INSTANCE(1, "终止流程"),
RETURN_USER_TASK(2, "驳回到指定任务节点");
private final Integer type;
private final String name;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmUserTaskRejectHandlerType::getType).toArray();
public static BpmUserTaskRejectHandlerType typeOf(Integer type) {
return ArrayUtil.firstMatch(item -> item.getType().equals(type), values());
}
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 用户任务超时处理类型枚举
*
* @author jason
*/
@Getter
@AllArgsConstructor
public enum BpmUserTaskTimeoutHandlerTypeEnum implements IntArrayValuable {
REMINDER(1,"自动提醒"),
APPROVE(2, "自动同意"),
REJECT(3, "自动拒绝");
private final Integer type;
private final String name;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmUserTaskTimeoutHandlerTypeEnum::getType).toArray();
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -14,7 +14,8 @@ public enum BpmMessageEnum {
PROCESS_INSTANCE_APPROVE("bpm_process_instance_approve"), // 流程任务被审批通过时发送给申请人
PROCESS_INSTANCE_REJECT("bpm_process_instance_reject"), // 流程任务被审批不通过时发送给申请人
TASK_ASSIGNED("bpm_task_assigned"); // 任务被分配时发送给审批人
TASK_ASSIGNED("bpm_task_assigned"), // 任务被分配时发送给审批人
TASK_TIMEOUT("bpm_task_timeout"); // 任务审批超时时发送给审批人
/**
* 短信模板的标识

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.bpm.enums.task;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import lombok.AllArgsConstructor;
import lombok.Getter;
@ -15,6 +16,7 @@ import java.util.Arrays;
@AllArgsConstructor
public enum BpmProcessInstanceStatusEnum implements IntArrayValuable {
NOT_START(-1, "未开始"),
RUNNING(1, "审批中"),
APPROVE(2, "审批通过"),
REJECT(3, "审批不通过"),
@ -36,4 +38,9 @@ public enum BpmProcessInstanceStatusEnum implements IntArrayValuable {
return ARRAYS;
}
public static boolean isProcessEndStatus(Integer status) {
return ObjectUtils.equalsAny(status,
APPROVE.getStatus(), REJECT.getStatus(), CANCEL.getStatus());
}
}

View File

@ -5,13 +5,13 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 流程实例/任务的删除原因枚举
* 流程实例/任务的的处理原因枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum BpmDeleteReasonEnum {
public enum BpmReasonEnum {
// ========== 流程实例的独有原因 ==========
@ -22,6 +22,16 @@ public enum BpmDeleteReasonEnum {
// ========== 流程任务的独有原因 ==========
CANCEL_BY_SYSTEM("系统自动取消"), // 场景非常多比如说1多任务审批已经满足条件无需审批该任务2流程实例被取消无需审批该任务等等
TIMEOUT_APPROVE("审批超时,系统自动通过"),
TIMEOUT_REJECT("审批超时,系统自动不通过"),
ASSIGN_START_USER_APPROVE("审批人与提交人为同一人时,自动通过"),
ASSIGN_START_USER_APPROVE_WHEN_SKIP("审批人与提交人为同一人时,自动通过"),
ASSIGN_START_USER_APPROVE_WHEN_DEPT_LEADER_NOT_FOUND("审批人与提交人为同一人时,找不到部门负责人,自动通过"),
ASSIGN_START_USER_TRANSFER_DEPT_LEADER("审批人与提交人为同一人时,转交给部门负责人审批"),
ASSIGN_EMPTY_APPROVE("审批人为空,自动通过"),
ASSIGN_EMPTY_REJECT("审批人为空,自动不通过"),
APPROVE_TYPE_AUTO_APPROVE("非人工审核,自动通过"),
APPROVE_TYPE_AUTO_REJECT("非人工审核,自动不通过"),
;
private final String reason;
@ -36,10 +46,4 @@ public enum BpmDeleteReasonEnum {
return StrUtil.format(reason, args);
}
// ========== 逻辑 ==========
public static boolean isRejectReason(String reason) {
return StrUtil.startWith(reason, "审批不通过任务,原因:");
}
}

View File

@ -13,6 +13,7 @@ import lombok.Getter;
@AllArgsConstructor
public enum BpmTaskStatusEnum {
NOT_START(-1, "未开始"),
RUNNING(1, "审批中"),
APPROVE(2, "审批通过"),
REJECT(3, "审批不通过"),

View File

@ -0,0 +1,4 @@
/**
* 基础包放一些通用的 VO
*/
package cn.iocoder.yudao.module.bpm.controller.admin.base;

View File

@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.bpm.controller.admin.base.user;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "用户精简信息 VO")
@Data
public class UserSimpleBaseVO {
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
private String nickname;
@Schema(description = "用户头像", example = "https://www.iocoder.cn/1.png")
private String avatar;
}

View File

@ -4,10 +4,9 @@ import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.io.IoUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.*;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelUpdateReqVO;
import cn.iocoder.yudao.module.bpm.convert.definition.BpmModelConvert;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
@ -15,10 +14,13 @@ import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService;
import cn.iocoder.yudao.module.bpm.service.definition.BpmFormService;
import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.Model;
import org.flowable.engine.repository.ProcessDefinition;
@ -26,17 +28,15 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - 流程模型")
@RestController
@ -53,6 +53,9 @@ public class BpmModelController {
@Resource
private BpmProcessDefinitionService processDefinitionService;
@Resource
private AdminUserApi adminUserApi;
@GetMapping("/page")
@Operation(summary = "获得模型分页")
public CommonResult<PageResult<BpmModelRespVO>> getModelPage(BpmModelPageReqVO pageVO) {
@ -64,7 +67,7 @@ public class BpmModelController {
// 拼接数据
// 获得 Form 表单
Set<Long> formIds = convertSet(pageResult.getList(), model -> {
BpmModelMetaInfoRespDTO metaInfo = JsonUtils.parseObject(model.getMetaInfo(), BpmModelMetaInfoRespDTO.class);
BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model);
return metaInfo != null ? metaInfo.getFormId() : null;
});
Map<Long, BpmFormDO> formMap = formService.getFormMap(formIds);
@ -78,7 +81,14 @@ public class BpmModelController {
// 获得 ProcessDefinition Map
List<ProcessDefinition> processDefinitions = processDefinitionService.getProcessDefinitionListByDeploymentIds(deploymentIds);
Map<String, ProcessDefinition> processDefinitionMap = convertMap(processDefinitions, ProcessDefinition::getDeploymentId);
return success(BpmModelConvert.INSTANCE.buildModelPage(pageResult, formMap, categoryMap, deploymentMap, processDefinitionMap));
// 获得 User Map
Set<Long> userIds = convertSetByFlatMap(pageResult.getList(), model -> {
BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model);
return metaInfo != null ? metaInfo.getStartUserIds().stream() : Stream.empty();
});
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
return success(BpmModelConvert.INSTANCE.buildModelPage(pageResult,
formMap, categoryMap, deploymentMap, processDefinitionMap, userMap));
}
@GetMapping("/get")
@ -97,34 +107,24 @@ public class BpmModelController {
@PostMapping("/create")
@Operation(summary = "新建模型")
@PreAuthorize("@ss.hasPermission('bpm:model:create')")
public CommonResult<String> createModel(@Valid @RequestBody BpmModelCreateReqVO createRetVO) {
return success(modelService.createModel(createRetVO, null));
public CommonResult<String> createModel(@Valid @RequestBody BpmModelSaveReqVO createRetVO) {
return success(modelService.createModel(createRetVO));
}
@PutMapping("/update")
@Operation(summary = "修改模型")
@PreAuthorize("@ss.hasPermission('bpm:model:update')")
public CommonResult<Boolean> updateModel(@Valid @RequestBody BpmModelUpdateReqVO modelVO) {
modelService.updateModel(modelVO);
public CommonResult<Boolean> updateModel(@Valid @RequestBody BpmModelSaveReqVO modelVO) {
modelService.updateModel(getLoginUserId(), modelVO);
return success(true);
}
@PostMapping("/import")
@Operation(summary = "导入模型")
@PreAuthorize("@ss.hasPermission('bpm:model:import')")
public CommonResult<String> importModel(@Valid BpmModeImportReqVO importReqVO) throws IOException {
BpmModelCreateReqVO createReqVO = BeanUtils.toBean(importReqVO, BpmModelCreateReqVO.class);
// 读取文件
String bpmnXml = IoUtils.readUtf8(importReqVO.getBpmnFile().getInputStream(), false);
return success(modelService.createModel(createReqVO, bpmnXml));
}
@PostMapping("/deploy")
@Operation(summary = "部署模型")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('bpm:model:deploy')")
public CommonResult<Boolean> deployModel(@RequestParam("id") String id) {
modelService.deployModel(id);
modelService.deployModel(getLoginUserId(), id);
return success(true);
}
@ -132,7 +132,15 @@ public class BpmModelController {
@Operation(summary = "修改模型的状态", description = "实际更新的部署的流程定义的状态")
@PreAuthorize("@ss.hasPermission('bpm:model:update')")
public CommonResult<Boolean> updateModelState(@Valid @RequestBody BpmModelUpdateStateReqVO reqVO) {
modelService.updateModelState(reqVO.getId(), reqVO.getState());
modelService.updateModelState(getLoginUserId(), reqVO.getId(), reqVO.getState());
return success(true);
}
@PutMapping("/update-bpmn")
@Operation(summary = "修改模型的 BPMN")
@PreAuthorize("@ss.hasPermission('bpm:model:update')")
public CommonResult<Boolean> updateModelBpmn(@Valid @RequestBody BpmModeUpdateBpmnReqVO reqVO) {
modelService.updateModelBpmnXml(reqVO.getId(), reqVO.getBpmnXml());
return success(true);
}
@ -141,8 +149,25 @@ public class BpmModelController {
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('bpm:model:delete')")
public CommonResult<Boolean> deleteModel(@RequestParam("id") String id) {
modelService.deleteModel(id);
modelService.deleteModel(getLoginUserId(), id);
return success(true);
}
// ========== 仿钉钉/飞书的精简模型 =========
@GetMapping("/simple/get")
@Operation(summary = "获得仿钉钉流程设计模型")
@Parameter(name = "modelId", description = "流程模型编号", required = true, example = "a2c5eee0-eb6c-11ee-abf4-0c37967c420a")
public CommonResult<BpmSimpleModelNodeVO> getSimpleModel(@RequestParam("id") String modelId){
return success(modelService.getSimpleModel(modelId));
}
@PostMapping("/simple/update")
@Operation(summary = "保存仿钉钉流程设计模型")
@PreAuthorize("@ss.hasPermission('bpm:model:update')")
public CommonResult<Boolean> updateSimpleModel(@Valid @RequestBody BpmSimpleModelUpdateReqVO reqVO) {
modelService.updateSimpleModel(getLoginUserId(), reqVO);
return success(Boolean.TRUE);
}
}

View File

@ -34,6 +34,7 @@ import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - 流程定义")
@RestController
@ -79,14 +80,23 @@ public class BpmProcessDefinitionController {
@Parameter(name = "suspensionState", description = "挂起状态", required = true, example = "1") // 参见 Flowable SuspensionState 枚举
public CommonResult<List<BpmProcessDefinitionRespVO>> getProcessDefinitionList(
@RequestParam("suspensionState") Integer suspensionState) {
// 1.1 获得开启的流程定义
List<ProcessDefinition> list = processDefinitionService.getProcessDefinitionListBySuspensionState(suspensionState);
if (CollUtil.isEmpty(list)) {
return success(Collections.emptyList());
}
// 获得 BpmProcessDefinitionInfoDO Map
// 1.2 移除不可见的流程定义
Map<String, BpmProcessDefinitionInfoDO> processDefinitionMap = processDefinitionService.getProcessDefinitionInfoMap(
convertSet(list, ProcessDefinition::getId));
Long userId = getLoginUserId();
list.removeIf(processDefinition -> {
BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionMap.get(processDefinition.getId());
return processDefinitionInfo == null // 不存在
|| Boolean.FALSE.equals(processDefinitionInfo.getVisible()) // visible 不可见
|| !processDefinitionService.canUserStartProcessDefinition(processDefinitionInfo, userId); // 无权限发起
});
// 2. 拼接 VO 返回
return success(BpmProcessDefinitionConvert.INSTANCE.buildProcessDefinitionList(
list, null, processDefinitionMap, null, null));
}

View File

@ -1,19 +0,0 @@
package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.constraints.NotNull;
@Schema(description = "管理后台 - 流程模型的导入 Request VO 相比流程模型的新建来说,只是多了一个 bpmnFile 文件")
@Data
public class BpmModeImportReqVO extends BpmModelCreateReqVO {
@Schema(description = "BPMN 文件", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "BPMN 文件不能为空")
private MultipartFile bpmnFile;
}

View File

@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
@Schema(description = "管理后台 - 流程模型的更新 BPMN XML Request VO")
@Data
public class BpmModeUpdateBpmnReqVO {
@Schema(description = "流程编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotEmpty(message = "流程编号不能为空")
private String id;
@Schema(description = "BPMN XML", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "BPMN XML 不能为空")
private String bpmnXml;
}

View File

@ -0,0 +1,62 @@
package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import java.util.List;
/**
* BPM 流程 MetaInfo Response DTO
* 主要用于 { Model#setMetaInfo(String)} 的存储
*
* 最终它的字段和 {@link cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO} 是一致的
*
* @author 芋道源码
*/
@Data
public class BpmModelMetaInfoVO {
@Schema(description = "流程图标", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/yudao.jpg")
@NotEmpty(message = "流程图标不能为空")
@URL(message = "流程图标格式不正确")
private String icon;
@Schema(description = "流程描述", example = "我是描述")
private String description;
@Schema(description = "流程类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
@InEnum(BpmModelTypeEnum.class)
@NotNull(message = "流程类型不能为空")
private Integer type;
@Schema(description = "表单类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
@InEnum(BpmModelFormTypeEnum.class)
@NotNull(message = "表单类型不能为空")
private Integer formType;
@Schema(description = "表单编号", example = "1024")
private Long formId; // formType NORMAL 使用必须非空
@Schema(description = "自定义表单的提交路径,使用 Vue 的路由地址",
example = "/bpm/oa/leave/create")
private String formCustomCreatePath; // 表单类型为 CUSTOM 必须非空
@Schema(description = "自定义表单的查看路径,使用 Vue 的路由地址",
example = "/bpm/oa/leave/view")
private String formCustomViewPath; // 表单类型为 CUSTOM 必须非空
@Schema(description = "是否可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
@NotNull(message = "是否可见不能为空")
private Boolean visible;
@Schema(description = "可发起用户编号数组", example = "[1,2,3]")
private List<Long> startUserIds;
@Schema(description = "可管理用户编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[2,4,6]")
@NotEmpty(message = "可管理用户编号数组不能为空")
private List<Long> managerUserIds;
}

View File

@ -1,14 +1,16 @@
package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model;
import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - 流程模型 Response VO")
@Data
public class BpmModelRespVO {
public class BpmModelRespVO extends BpmModelMetaInfoVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private String id;
@ -22,33 +24,23 @@ public class BpmModelRespVO {
@Schema(description = "流程图标", example = "https://www.iocoder.cn/yudao.jpg")
private String icon;
@Schema(description = "流程描述", example = "我是描述")
private String description;
@Schema(description = "流程分类编码", example = "1")
private String category;
@Schema(description = "流程分类名字", example = "请假")
private String categoryName;
@Schema(description = "表单类型-参见 bpm_model_form_type 数据字典", example = "1")
private Integer formType;
@Schema(description = "表单编号", example = "1024")
private Long formId; // 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 必须非空
@Schema(description = "表单名字", example = "请假表单")
private String formName;
@Schema(description = "自定义表单的提交路径", example = "/bpm/oa/leave/create")
private String formCustomCreatePath; // 使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 必须非空
@Schema(description = "自定义表单的查看路径", example = "/bpm/oa/leave/view")
private String formCustomViewPath; // 使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 必须非空
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
@Schema(description = "BPMN XML", requiredMode = Schema.RequiredMode.REQUIRED)
private String bpmnXml;
@Schema(description = "可发起的用户数组")
private List<UserSimpleBaseVO> startUsers;
/**
* 最新部署的流程定义
*/

View File

@ -1,15 +1,15 @@
package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import javax.validation.constraints.NotEmpty;
@Schema(description = "管理后台 - 流程模型的创建 Request VO")
@Schema(description = "管理后台 - 流程模型的保存 Request VO")
@Data
public class BpmModelCreateReqVO {
public class BpmModelSaveReqVO extends BpmModelMetaInfoVO {
@Schema(description = "编号", example = "1024")
private String id;
@Schema(description = "流程标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "process_yudao")
@NotEmpty(message = "流程标识不能为空")
@ -19,7 +19,7 @@ public class BpmModelCreateReqVO {
@NotEmpty(message = "流程名称不能为空")
private String name;
@Schema(description = "流程描述", example = "我是描述")
private String description;
@Schema(description = "流程分类", example = "1")
private String category;
}

View File

@ -1,47 +0,0 @@
package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.NotEmpty;
@Schema(description = "管理后台 - 流程模型的更新 Request VO")
@Data
public class BpmModelUpdateReqVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotEmpty(message = "编号不能为空")
private String id;
@Schema(description = "流程名称", example = "芋道")
private String name;
@Schema(description = "流程图标", example = "https://www.iocoder.cn/yudao.jpg")
@URL(message = "流程图标格式不正确")
private String icon;
@Schema(description = "流程描述", example = "我是描述")
private String description;
@Schema(description = "流程分类", example = "1")
private String category;
@Schema(description = "BPMN XML", requiredMode = Schema.RequiredMode.REQUIRED)
private String bpmnXml;
@Schema(description = "表单类型-参见 bpm_model_form_type 数据字典", example = "1")
@InEnum(BpmModelFormTypeEnum.class)
private Integer formType;
@Schema(description = "表单编号-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", example = "1024")
private Long formId;
@Schema(description = "自定义表单的提交路径,使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空",
example = "/bpm/oa/leave/create")
private String formCustomCreatePath;
@Schema(description = "自定义表单的查看路径,使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空",
example = "/bpm/oa/leave/view")
private String formCustomViewPath;
}

View File

@ -0,0 +1,220 @@
package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.*;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Schema(description = "管理后台 - 仿钉钉流程设计模型节点 VO")
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class BpmSimpleModelNodeVO {
@Schema(description = "模型节点编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "StartEvent_1")
@NotEmpty(message = "模型节点编号不能为空")
private String id;
@Schema(description = "模型节点类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "模型节点类型不能为空")
@InEnum(BpmSimpleModelNodeType.class)
private Integer type;
@Schema(description = "模型节点名称", example = "领导审批")
private String name;
// TODO @jason gpt 大模型对了下这个字段的命名貌似叫 displayText 合适点可以等最后我们全局替换下优先级
@Schema(description = "节点展示内容", example = "指定成员: 芋道源码")
private String showText;
@Schema(description = "子节点")
private BpmSimpleModelNodeVO childNode; // 补充说明在该模型下子节点有且仅有一个不会有多个
@Schema(description = "条件节点")
private List<BpmSimpleModelNodeVO> conditionNodes; // 补充说明有且仅有条件并行包容等分支会使用
@Schema(description = "条件类型", example = "1")
@InEnum(BpmSimpleModeConditionType.class)
private Integer conditionType; // 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE
@Schema(description = "条件表达式", example = "${day>3}")
private String conditionExpression; // 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE
@Schema(description = "是否默认条件", example = "true")
private Boolean defaultFlow; // 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE
/**
* 条件组
*/
private ConditionGroups conditionGroups; // 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE
@Schema(description = "候选人策略", example = "30")
@InEnum(BpmTaskCandidateStrategyEnum.class)
private Integer candidateStrategy; // 用于审批抄送节点
@Schema(description = "候选人参数")
private String candidateParam; // 用于审批抄送节点
@Schema(description = "审批节点类型", example = "1")
@InEnum(BpmUserTaskApproveTypeEnum.class)
private Integer approveType; // 用于审批节点
@Schema(description = "多人审批方式", example = "1")
@InEnum(BpmUserTaskApproveMethodEnum.class)
private Integer approveMethod; // 用于审批节点
@Schema(description = "通过比例", example = "100")
private Integer approveRatio; // 通过比例当多人审批方式为多人会签(按通过比例) 需要设置
@Schema(description = "表单权限", example = "[]")
private List<Map<String, String>> fieldsPermission;
@Schema(description = "操作按钮设置", example = "[]")
private List<OperationButtonSetting> buttonsSetting; // 用于审批节点
// TODO @jason看看是不是可以简化@芋艿 暂时先放着不知道后面是否会用到
/**
* 附加节点 Id, 该节点不从前端传入 由程序生成. 由于当个节点无法完成功能 需要附加节点来完成
*/
@JsonIgnore
private String attachNodeId;
/**
* 审批节点拒绝处理
*/
private RejectHandler rejectHandler;
/**
* 审批节点超时处理
*/
private TimeoutHandler timeoutHandler;
@Schema(description = "审批节点的审批人与发起人相同时,对应的处理类型", example = "1")
@InEnum(BpmUserTaskAssignStartUserHandlerTypeEnum.class)
private Integer assignStartUserHandlerType;
/**
* 空处理策略
*/
private AssignEmptyHandler assignEmptyHandler;
@Schema(description = "审批节点拒绝处理策略")
@Data
public static class RejectHandler {
@Schema(description = "拒绝处理类型", example = "1")
@InEnum(BpmUserTaskRejectHandlerType.class)
private Integer type;
@Schema(description = "任务拒绝后驳回的节点 Id", example = "Activity_1")
private String returnNodeId;
}
@Schema(description = "审批节点超时处理策略")
@Valid
@Data
public static class TimeoutHandler {
@Schema(description = "是否开启超时处理", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
@NotNull(message = "是否开启超时处理不能为空")
private Boolean enable;
@Schema(description = "任务超时未处理的行为", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "任务超时未处理的行为不能为空")
@InEnum(BpmUserTaskTimeoutHandlerTypeEnum.class)
private Integer type;
@Schema(description = "超时时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "PT6H")
@NotEmpty(message = "超时时间不能为空")
private String timeDuration;
@Schema(description = "最大提醒次数", example = "1")
private Integer maxRemindCount;
}
@Schema(description = "空处理策略")
@Data
@Valid
public static class AssignEmptyHandler {
@Schema(description = "空处理类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "空处理类型不能为空")
@InEnum(BpmUserTaskAssignEmptyHandlerTypeEnum.class)
private Integer type;
@Schema(description = "指定人员审批的用户编号数组", example = "1")
private List<Long> userIds;
}
@Schema(description = "操作按钮设置")
@Data
@Valid
public static class OperationButtonSetting {
// TODO @jason是不是按钮的标识id 会和数据库的 id 自增有点模糊key 标识会更合理一点点哈
@Schema(description = "按钮 Id", example = "1")
private Integer id;
@Schema(description = "显示名称", example = "审批")
private String displayName;
@Schema(description = "是否启用", example = "true")
private Boolean enable;
}
@Schema(description = "条件组")
@Data
@Valid
public static class ConditionGroups {
@Schema(description = "条件组下的条件关系是否为与关系", example = "true")
@NotNull(message = "条件关系不能为空")
private Boolean and;
@Schema(description = "条件组下的条件", example = "[]")
@NotEmpty(message = "条件不能为空")
private List<Condition> conditions;
}
@Schema(description = "条件")
@Data
@Valid
public static class Condition {
@Schema(description = "条件下的规则关系是否为与关系", example = "true")
@NotNull(message = "规则关系不能为空")
private Boolean and;
@Schema(description = "条件下的规则", example = "[]")
@NotEmpty(message = "规则不能为空")
private List<ConditionRule> rules;
}
@Schema(description = "条件规则")
@Data
@Valid
public static class ConditionRule {
@Schema(description = "运行符号", example = "==")
@NotEmpty(message = "运行符号不能为空")
private String opCode;
@Schema(description = "运算符左边的值,例如某个流程变量", example = "startUserId")
@NotEmpty(message = "运算符左边的值不能为空")
private String leftSide;
@Schema(description = "运算符右边的值", example = "1")
@NotEmpty(message = "运算符右边的值不能为空")
private String rightSide;
}
// TODO @芋艿条件建议可以固化的一些选项然后有个表达式兜底要支持
}

View File

@ -0,0 +1,23 @@
package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
// TODO @jason需要考虑如果某个节点的配置不正确需要有提示具体怎么实现可以讨论下
@Schema(description = "管理后台 - 仿钉钉流程设计模型的新增/修改 Request VO")
@Data
public class BpmSimpleModelUpdateReqVO {
@Schema(description = "流程模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotEmpty(message = "流程模型编号不能为空")
private String id; // 对应 Flowable act_re_model ID_ 字段
@Schema(description = "仿钉钉流程设计模型对象", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "仿钉钉流程设计模型对象不能为空")
@Valid
private BpmSimpleModelNodeVO simpleModel;
}

View File

@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.controller.admin.task;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.activity.BpmActivityRespVO;
import cn.iocoder.yudao.module.bpm.convert.task.BpmActivityConvert;
import cn.iocoder.yudao.module.bpm.service.task.BpmActivityService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
@ -34,6 +35,6 @@ public class BpmActivityController {
@PreAuthorize("@ss.hasPermission('bpm:task:query')")
public CommonResult<List<BpmActivityRespVO>> getActivityList(
@RequestParam("processInstanceId") String processInstanceId) {
return success(activityService.getActivityListByProcessInstanceId(processInstanceId));
return success(BpmActivityConvert.INSTANCE.convertList(activityService.getActivityListByProcessInstanceId(processInstanceId)));
}
}

View File

@ -4,10 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCancelReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCreateReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstancePageReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceRespVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*;
import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
@ -160,4 +157,20 @@ public class BpmProcessInstanceController {
return success(true);
}
@GetMapping("/get-form-fields-permission")
@Operation(summary = "获得表单字段权限")
@PreAuthorize("@ss.hasPermission('bpm:process-instance:query')")
public CommonResult<Map<String, String>> getFormFieldsPermission(
@Valid BpmFormFieldsPermissionReqVO reqVO) {
return success(processInstanceService.getFormFieldsPermission(reqVO));
}
@GetMapping("/get-approval-detail")
@Operation(summary = "获得审批详情")
@Parameter(name = "id", description = "流程实例的编号", required = true)
@PreAuthorize("@ss.hasPermission('bpm:process-instance:query')")
public CommonResult<BpmApprovalDetailRespVO> getApprovalDetail(@Valid BpmApprovalDetailReqVO reqVO) {
return success(processInstanceService.getApprovalDetail(getLoginUserId(), reqVO));
}
}

View File

@ -11,7 +11,6 @@ import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessI
import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceCopyService;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
@ -43,8 +42,6 @@ public class BpmProcessInstanceCopyController {
private BpmProcessInstanceCopyService processInstanceCopyService;
@Resource
private BpmProcessInstanceService processInstanceService;
@Resource
private BpmTaskService taskService;
@Resource
private AdminUserApi adminUserApi;
@ -61,8 +58,6 @@ public class BpmProcessInstanceCopyController {
}
// 拼接返回
Map<String, String> taskNameMap = taskService.getTaskNameByTaskIds(
convertSet(pageResult.getList(), BpmProcessInstanceCopyDO::getTaskId));
Map<String, HistoricProcessInstance> processInstanceMap = processInstanceService.getHistoricProcessInstanceMap(
convertSet(pageResult.getList(), BpmProcessInstanceCopyDO::getProcessInstanceId));
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(pageResult.getList(),
@ -70,7 +65,6 @@ public class BpmProcessInstanceCopyController {
return success(BeanUtils.toBean(pageResult, BpmProcessInstanceCopyRespVO.class, copyVO -> {
MapUtils.findAndThen(userMap, Long.valueOf(copyVO.getCreator()), user -> copyVO.setCreatorName(user.getNickname()));
MapUtils.findAndThen(userMap, copyVO.getStartUserId(), user -> copyVO.setStartUserName(user.getNickname()));
MapUtils.findAndThen(taskNameMap, copyVO.getTaskId(), copyVO::setTaskName);
MapUtils.findAndThen(processInstanceMap, copyVO.getProcessInstanceId(),
processInstance -> copyVO.setProcessInstanceStartTime(DateUtils.of(processInstance.getStartTime())));
}));

View File

@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*;
import cn.iocoder.yudao.module.bpm.convert.task.BpmTaskConvert;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
import cn.iocoder.yudao.module.bpm.service.definition.BpmFormService;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
@ -17,6 +18,9 @@ import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.UserTask;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.runtime.ProcessInstance;
@ -26,8 +30,6 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -50,6 +52,8 @@ public class BpmTaskController {
private BpmProcessInstanceService processInstanceService;
@Resource
private BpmFormService formService;
@Resource
private BpmProcessDefinitionService bpmProcessDefinitionService;
@Resource
private AdminUserApi adminUserApi;
@ -134,8 +138,10 @@ public class BpmTaskController {
// 获得 Form Map
Map<Long, BpmFormDO> formMap = formService.getFormMap(
convertSet(taskList, task -> NumberUtils.parseLong(task.getFormKey())));
// 获得 BpmnModel
BpmnModel bpmnModel = bpmProcessDefinitionService.getProcessDefinitionBpmnModel(processInstance.getProcessDefinitionId());
return success(BpmTaskConvert.INSTANCE.buildTaskListByProcessInstanceId(taskList, processInstance,
formMap, userMap, deptMap));
formMap, userMap, deptMap, bpmnModel));
}
@PutMapping("/approve")

View File

@ -24,6 +24,8 @@ public class BpmProcessInstanceCopyRespVO {
@Schema(description = "流程实例的发起时间")
private LocalDateTime processInstanceStartTime;
@Schema(description = "抄送的节点的活动编号")
private String activityId;
@Schema(description = "发起抄送的任务编号")
private String taskId;
@Schema(description = "发起抄送的任务名称")

View File

@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.AssertTrue;
import lombok.Data;
// TODO @jason这个可以简化下使用 @RequestParam嘿嘿主要 VO 项不要太多
@Schema(description = "管理后台 - 审批详情 Request VO")
@Data
public class BpmApprovalDetailReqVO {
@Schema(description = "流程定义的编号", example = "1024")
private String processDefinitionId;
@Schema(description = "流程实例的编号", example = "1024")
private String processInstanceId;
@AssertTrue(message = "流程定义的编号和流程实例的编号不能同时为空")
@JsonIgnore
public boolean isValidProcessParam() {
return StrUtil.isNotEmpty(processDefinitionId) || StrUtil.isNotEmpty(processInstanceId);
}
}

View File

@ -0,0 +1,87 @@
package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - 审批详情 Response VO")
@Data
public class BpmApprovalDetailRespVO {
@Schema(description = "流程实例的状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer status; // 参见 BpmProcessInstanceStatusEnum 枚举
@Schema(description = "审批信息列表", requiredMode = Schema.RequiredMode.REQUIRED)
private List<ApprovalNodeInfo> approveNodes;
@Schema(description = "审批节点信息")
@Data
public static class ApprovalNodeInfo {
@Schema(description = "节点编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "StartUserNode")
private String id;
@Schema(description = "节点名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "发起人")
private String name;
@Schema(description = "节点类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer nodeType; // 参见 BpmSimpleModelNodeType 枚举
@Schema(description = "节点状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer status; // 参见 BpmTaskStatusEnum 枚举
@Schema(description = "节点的开始时间")
private LocalDateTime startTime;
@Schema(description = "节点的结束时间")
private LocalDateTime endTime;
@Schema(description = "审批节点的任务信息")
private List<ApprovalTaskInfo> tasks;
@Schema(description = "候选人用户列表")
// TODO @jasoncandidateUserList => candidateUsers保持和 tasks 的命名风格一致哈
private List<User> candidateUserList; // 用于未运行任务节点
}
// TODO @jason可以替换成 UserSimpleBaseVO简化下
@Schema(description = "用户信息")
@Data
public static class User {
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
private String nickname;
@Schema(description = "用户头像", example = "https://www.iocoder.cn/1.png")
private String avatar;
}
@Schema(description = "审批任务信息")
@Data
public static class ApprovalTaskInfo {
@Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private String id;
@Schema(description = "任务所属人", example = "1024")
private User ownerUser;
@Schema(description = "任务分配人", example = "2048")
private User assigneeUser;
@Schema(description = "任务状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer status; // 参见 BpmTaskStatusEnum 枚举
@Schema(description = "审批意见", example = "同意")
private String reason;
}
}

View File

@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.AssertTrue;
import lombok.Data;
@Schema(description = "管理后台 - 表单字段权限 Request VO")
@Data
public class BpmFormFieldsPermissionReqVO {
@Schema(description = "流程定义的编号", example = "1024")
private String processDefinitionId;
@Schema(description = "流程实例的编号", example = "1024")
private String processInstanceId;
@Schema(description = "流程活动编号", example = "StartUserNode")
private String activityId; // 对应 BPMN XML 节点 Id
@Schema(description = "流程任务编号", example = "95f2f08b-621b-11ef-bf39-00ff4722db8b")
private String taskId; // UserTask 对应的Id
@AssertTrue(message = "流程定义的编号和流程实例的编号不能同时为空")
@JsonIgnore
public boolean isValidProcessParam() {
return StrUtil.isNotEmpty(processDefinitionId) || StrUtil.isNotEmpty(processInstanceId);
}
@AssertTrue(message = "流程活动编号和流程任务编号编号不能同时为空")
@JsonIgnore
public boolean isValidActivityParam() {
return StrUtil.isNotEmpty(activityId) || StrUtil.isNotEmpty(taskId);
}
}

View File

@ -61,12 +61,17 @@ public class BpmTaskRespVO {
private Long formId;
@Schema(description = "表单名字", example = "请假表单")
private String formName;
@Schema(description = "表单的配置-JSON 字符串")
@Schema(description = "表单的配置JSON 字符串")
private String formConf;
@Schema(description = "表单项的数组")
private List<String> formFields;
@Schema(description = "提交的表单值", requiredMode = Schema.RequiredMode.REQUIRED)
private Map<String, Object> formVariables;
// @芋艿 都改成了 fieldsPermission buttonsSetting BpmSimpleModelNodeVO 统一
@Schema(description = "表单字段权限值")
private Map<String, String> fieldsPermission;
@Schema(description = "操作按钮设置值")
private Map<Integer, OperationButtonSetting> buttonsSetting;
@Data
@Schema(description = "流程实例")
@ -91,4 +96,15 @@ public class BpmTaskRespVO {
}
@Data
@Schema(description = "操作按钮设置")
public static class OperationButtonSetting {
@Schema(description = "显示名称", example = "审批")
private String displayName;
@Schema(description = "是否启用", example = "true")
private Boolean enable;
}
}

View File

@ -1,20 +1,19 @@
package cn.iocoder.yudao.module.bpm.convert.definition;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelCreateReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelRespVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelUpdateReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelSaveReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import org.flowable.common.engine.impl.db.SuspensionState;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.Model;
@ -22,9 +21,11 @@ import org.flowable.engine.repository.ProcessDefinition;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
/**
* 流程模型 Convert
@ -39,22 +40,24 @@ public interface BpmModelConvert {
default PageResult<BpmModelRespVO> buildModelPage(PageResult<Model> pageResult,
Map<Long, BpmFormDO> formMap,
Map<String, BpmCategoryDO> categoryMap, Map<String, Deployment> deploymentMap,
Map<String, ProcessDefinition> processDefinitionMap) {
List<BpmModelRespVO> list = CollectionUtils.convertList(pageResult.getList(), model -> {
BpmModelMetaInfoRespDTO metaInfo = buildMetaInfo(model);
Map<String, ProcessDefinition> processDefinitionMap,
Map<Long, AdminUserRespDTO> userMap) {
List<BpmModelRespVO> list = convertList(pageResult.getList(), model -> {
BpmModelMetaInfoVO metaInfo = parseMetaInfo(model);
BpmFormDO form = metaInfo != null ? formMap.get(metaInfo.getFormId()) : null;
BpmCategoryDO category = categoryMap.get(model.getCategory());
Deployment deployment = model.getDeploymentId() != null ? deploymentMap.get(model.getDeploymentId()) : null;
ProcessDefinition processDefinition = model.getDeploymentId() != null ? processDefinitionMap.get(model.getDeploymentId()) : null;
return buildModel0(model, metaInfo, form, category, deployment, processDefinition);
List<AdminUserRespDTO> startUsers = metaInfo != null ? convertList(metaInfo.getStartUserIds(), userMap::get) : null;
return buildModel0(model, metaInfo, form, category, deployment, processDefinition, startUsers);
});
return new PageResult<>(list, pageResult.getTotal());
}
default BpmModelRespVO buildModel(Model model,
byte[] bpmnBytes) {
BpmModelMetaInfoRespDTO metaInfo = buildMetaInfo(model);
BpmModelRespVO modelVO = buildModel0(model, metaInfo, null, null, null, null);
BpmModelMetaInfoVO metaInfo = parseMetaInfo(model);
BpmModelRespVO modelVO = buildModel0(model, metaInfo, null, null, null, null, null);
if (ArrayUtil.isNotEmpty(bpmnBytes)) {
modelVO.setBpmnXml(BpmnModelUtils.getBpmnXml(bpmnBytes));
}
@ -62,20 +65,16 @@ public interface BpmModelConvert {
}
default BpmModelRespVO buildModel0(Model model,
BpmModelMetaInfoRespDTO metaInfo, BpmFormDO form, BpmCategoryDO category,
Deployment deployment, ProcessDefinition processDefinition) {
BpmModelMetaInfoVO metaInfo, BpmFormDO form, BpmCategoryDO category,
Deployment deployment, ProcessDefinition processDefinition,
List<AdminUserRespDTO> startUsers) {
BpmModelRespVO modelRespVO = new BpmModelRespVO().setId(model.getId()).setName(model.getName())
.setKey(model.getKey()).setCategory(model.getCategory())
.setCreateTime(DateUtils.of(model.getCreateTime()));
// Form
if (metaInfo != null) {
modelRespVO.setFormType(metaInfo.getFormType()).setFormId(metaInfo.getFormId())
.setFormCustomCreatePath(metaInfo.getFormCustomCreatePath())
.setFormCustomViewPath(metaInfo.getFormCustomViewPath());
modelRespVO.setIcon(metaInfo.getIcon()).setDescription(metaInfo.getDescription());
}
BeanUtils.copyProperties(metaInfo, modelRespVO);
if (form != null) {
modelRespVO.setFormId(form.getId()).setFormName(form.getName());
modelRespVO.setFormName(form.getName());
}
// Category
if (category != null) {
@ -90,49 +89,30 @@ public interface BpmModelConvert {
modelRespVO.getProcessDefinition().setDeploymentTime(DateUtils.of(deployment.getDeploymentTime()));
}
}
// User
modelRespVO.setStartUsers(BeanUtils.toBean(startUsers, UserSimpleBaseVO.class));
return modelRespVO;
}
default void copyToCreateModel(Model model, BpmModelCreateReqVO bean) {
model.setName(bean.getName());
model.setKey(bean.getKey());
model.setMetaInfo(buildMetaInfoStr(null,
null, bean.getDescription(),
null, null, null, null));
default void copyToModel(Model model, BpmModelSaveReqVO reqVO) {
model.setName(reqVO.getName());
model.setKey(reqVO.getKey());
model.setCategory(reqVO.getCategory());
model.setMetaInfo(JsonUtils.toJsonString(BeanUtils.toBean(reqVO, BpmModelMetaInfoVO.class)));
}
default void copyToUpdateModel(Model model, BpmModelUpdateReqVO bean) {
model.setName(bean.getName());
model.setCategory(bean.getCategory());
model.setMetaInfo(buildMetaInfoStr(buildMetaInfo(model),
bean.getIcon(), bean.getDescription(),
bean.getFormType(), bean.getFormId(), bean.getFormCustomCreatePath(), bean.getFormCustomViewPath()));
}
default String buildMetaInfoStr(BpmModelMetaInfoRespDTO metaInfo,
String icon, String description,
Integer formType, Long formId, String formCustomCreatePath, String formCustomViewPath) {
if (metaInfo == null) {
metaInfo = new BpmModelMetaInfoRespDTO();
default BpmModelMetaInfoVO parseMetaInfo(Model model) {
BpmModelMetaInfoVO vo = JsonUtils.parseObject(model.getMetaInfo(), BpmModelMetaInfoVO.class);
if (vo == null) {
return null;
}
// 只有非空才进行设置避免更新时的覆盖
if (StrUtil.isNotEmpty(icon)) {
metaInfo.setIcon(icon);
if (vo.getManagerUserIds() == null) {
vo.setManagerUserIds(Collections.emptyList());
}
if (StrUtil.isNotEmpty(description)) {
metaInfo.setDescription(description);
if (vo.getStartUserIds() == null) {
vo.setStartUserIds(Collections.emptyList());
}
if (Objects.nonNull(formType)) {
metaInfo.setFormType(formType);
metaInfo.setFormId(formId);
metaInfo.setFormCustomCreatePath(formCustomCreatePath);
metaInfo.setFormCustomViewPath(formCustomViewPath);
}
return JsonUtils.toJsonString(metaInfo);
}
default BpmModelMetaInfoRespDTO buildMetaInfo(Model model) {
return JsonUtils.parseObject(model.getMetaInfo(), BpmModelMetaInfoRespDTO.class);
return vo;
}
}

View File

@ -89,11 +89,6 @@ public interface BpmProcessInstanceConvert {
@Mapping(source = "from.id", target = "to.id", ignore = true)
void copyTo(BpmProcessDefinitionInfoDO from, @MappingTarget BpmProcessDefinitionRespVO to);
default BpmProcessInstanceStatusEvent buildProcessInstanceStatusEvent(Object source, HistoricProcessInstance instance, Integer status) {
return new BpmProcessInstanceStatusEvent(source).setId(instance.getId()).setStatus(status)
.setProcessDefinitionKey(instance.getProcessDefinitionKey()).setBusinessKey(instance.getBusinessKey());
}
default BpmProcessInstanceStatusEvent buildProcessInstanceStatusEvent(Object source, ProcessInstance instance, Integer status) {;
return new BpmProcessInstanceStatusEvent(source).setId(instance.getId()).setStatus(status)
.setProcessDefinitionKey(instance.getProcessDefinitionKey()).setBusinessKey(instance.getBusinessKey());

View File

@ -9,10 +9,13 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceRespVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskCreatedReqDTO;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
@ -81,10 +84,12 @@ public interface BpmTaskConvert {
HistoricProcessInstance processInstance,
Map<Long, BpmFormDO> formMap,
Map<Long, AdminUserRespDTO> userMap,
Map<Long, DeptRespDTO> deptMap) {
Map<Long, DeptRespDTO> deptMap,
BpmnModel bpmnModel) {
List<BpmTaskRespVO> taskVOList = CollectionUtils.convertList(taskList, task -> {
BpmTaskRespVO taskVO = BeanUtils.toBean(task, BpmTaskRespVO.class);
taskVO.setStatus(FlowableUtils.getTaskStatus(task)).setReason(FlowableUtils.getTaskReason(task));
Integer taskStatus = FlowableUtils.getTaskStatus(task);
taskVO.setStatus(taskStatus).setReason(FlowableUtils.getTaskReason(task));
// 流程实例
AdminUserRespDTO startUser = userMap.get(NumberUtils.parseLong(processInstance.getStartUserId()));
taskVO.setProcessInstance(BeanUtils.toBean(processInstance, BpmTaskRespVO.ProcessInstance.class));
@ -106,7 +111,15 @@ public interface BpmTaskConvert {
taskVO.setOwnerUser(BeanUtils.toBean(ownerUser, BpmProcessInstanceRespVO.User.class));
findAndThen(deptMap, ownerUser.getDeptId(), dept -> taskVO.getOwnerUser().setDeptName(dept.getName()));
}
return taskVO;
if (BpmTaskStatusEnum.RUNNING.getStatus().equals(taskStatus)){
// 设置表单权限 TODO @芋艿 是不是还要加一个全局的权限 基于 processInstance 的权限回复可能不需要但是发起人需要有个权限配置
// TODO @jason貌似这么返回主要解决当前审批 task 的表单权限但是不同抄送人的表单权限可能不太对例如说 A 抄送人是隐藏某个字段
// @芋艿 表单权限需要分离开单独的接口来获取了 BpmProcessInstanceService.getProcessInstanceFormFieldsPermission
taskVO.setFieldsPermission(BpmnModelUtils.parseFormFieldsPermission(bpmnModel, task.getTaskDefinitionKey()));
// 操作按钮设置
taskVO.setButtonsSetting(BpmnModelUtils.parseButtonsSetting(bpmnModel, task.getTaskDefinitionKey()));
}
return taskVO;
});
// 拼接父子关系
@ -151,12 +164,12 @@ public interface BpmTaskConvert {
/**
* 将父任务的属性拷贝到子任务加签任务
*
* <p>
* 为什么不使用 mapstruct 映射因为 TaskEntityImpl 还有很多其他属性这里我们只设置我们需要的
* 使用 mapstruct 会将里面嵌套的各个属性值都设置进去会出现意想不到的问题
*
* @param parentTask 父任务
* @param childTask 加签任务
* @param childTask 加签任务
*/
default void copyTo(TaskEntityImpl parentTask, TaskEntityImpl childTask) {
childTask.setName(parentTask.getName());
@ -165,7 +178,6 @@ public interface BpmTaskConvert {
childTask.setParentTaskId(parentTask.getId());
childTask.setProcessDefinitionId(parentTask.getProcessDefinitionId());
childTask.setProcessInstanceId(parentTask.getProcessInstanceId());
// childTask.setExecutionId(parentTask.getExecutionId()); // TODO 芋艿新加的不太确定尴尬不加时子任务不通过会失败报错加了子任务审批通过会失败报错
childTask.setTaskDefinitionKey(parentTask.getTaskDefinitionKey());
childTask.setTaskDefinitionId(parentTask.getTaskDefinitionId());
childTask.setPriority(parentTask.getPriority());

View File

@ -1,12 +1,20 @@
package cn.iocoder.yudao.module.bpm.dal.dataobject.definition;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.flowable.engine.repository.Model;
import org.flowable.engine.repository.ProcessDefinition;
import java.util.List;
@ -31,15 +39,21 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
/**
* 流程定义的编号
*
* 关联 ProcessDefinition id 属性
* 关联 {@link ProcessDefinition#getId()} 属性
*/
private String processDefinitionId;
/**
* 流程模型的编号
*
* 关联 Model id 属性
* 关联 {@link Model#getId()} 属性
*/
private String modelId;
/**
* 流程模型的类型
*
* 枚举 {@link BpmModelTypeEnum}
*/
private Integer modelType;
/**
* 图标
@ -53,11 +67,12 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
/**
* 表单类型
*
* 关联 {@link BpmModelFormTypeEnum}
* 枚举 {@link BpmModelFormTypeEnum}
*/
private Integer formType;
/**
* 动态表单编号
*
* 在表单类型为 {@link BpmModelFormTypeEnum#NORMAL}
*
* 关联 {@link BpmFormDO#getId()}
@ -65,6 +80,7 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
private Long formId;
/**
* 表单的配置
*
* 在表单类型为 {@link BpmModelFormTypeEnum#NORMAL}
*
* 冗余 {@link BpmFormDO#getConf()}
@ -72,21 +88,59 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
private String formConf;
/**
* 表单项的数组
*
* 在表单类型为 {@link BpmModelFormTypeEnum#NORMAL}
*
* 冗余 {@link BpmFormDO#getFields()} ()}
* 冗余 {@link BpmFormDO#getFields()}
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> formFields;
/**
* 自定义表单的提交路径使用 Vue 的路由地址
*
* 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM}
*/
private String formCustomCreatePath;
/**
* 自定义表单的查看路径使用 Vue 的路由地址
*
* 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM}
*/
private String formCustomViewPath;
/**
* SIMPLE 设计器模型数据 json 格式
*
* 目的当使用仿钉钉设计器时流程模型发布的时候需要保存流程模型设计器的快照数据
*/
private String simpleModel;
/**
* 是否可见
*
* 目的如果 false 不可见则不展示在发起流程的列表里
*/
private Boolean visible;
/**
* 可发起用户编号数组
*
* 关联 {@link AdminUserRespDTO#getId()} 字段的数组
*
* 如果为空则表示全部可以发起
*
* 它和 {@link #visible} 的区别在于
* 1. {@link #visible} 只是决定是否可见即使不可见还是可以发起
* 2. startUserIds 决定某个用户是否可以发起如果该用户不可发起则他也是不可见的
*/
@TableField(typeHandler = StringListTypeHandler.class) // 为了可以使用 find_in_set 进行过滤
private List<Long> startUserIds;
/**
* 可管理用户编号数组
*
* 关联 {@link AdminUserRespDTO#getId()} 字段的数组
*/
@TableField(typeHandler = StringListTypeHandler.class) // 为了可以使用 find_in_set 进行过滤
private List<Long> managerUserIds;
}

View File

@ -48,10 +48,15 @@ public class BpmProcessInstanceCopyDO extends BaseDO {
* 冗余 ProcessInstance category 字段
*/
private String category;
/**
* 流程活动编号
* <p/>
* 对应 BPMN XML 节点编号用于查询抄送节点的表单字段权限
* 这里冗余的原因如果是钉钉易搭的抄送节点 (ServiceTask)使用 taskId 可能查不到对应的 activityId
*/
private String activityId;
/**
* 任务主键
*
* 关联 Task id 属性
*/
private String taskId;

View File

@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessI
import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface BpmProcessInstanceCopyMapper extends BaseMapperX<BpmProcessInstanceCopyDO> {
@ -18,4 +20,9 @@ public interface BpmProcessInstanceCopyMapper extends BaseMapperX<BpmProcessInst
.orderByDesc(BpmProcessInstanceCopyDO::getId));
}
default List<BpmProcessInstanceCopyDO> selectListByProcessInstanceIdAndActivityId(String processInstanceId, String activityId) {
return selectList(BpmProcessInstanceCopyDO::getProcessInstanceId, processInstanceId,
BpmProcessInstanceCopyDO::getActivityId, activityId);
}
}

View File

@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCand
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.event.BpmProcessInstanceEventPublisher;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import org.flowable.common.engine.api.delegate.FlowableFunctionDelegate;
import org.flowable.common.engine.api.delegate.event.FlowableEventListener;
import org.flowable.spring.SpringProcessEngineConfiguration;
import org.flowable.spring.boot.EngineConfigurationConfigurer;
@ -56,12 +57,15 @@ public class BpmFlowableConfiguration {
@Bean
public EngineConfigurationConfigurer<SpringProcessEngineConfiguration> bpmProcessEngineConfigurationConfigurer(
ObjectProvider<FlowableEventListener> listeners,
ObjectProvider<FlowableFunctionDelegate> customFlowableFunctionDelegates,
BpmActivityBehaviorFactory bpmActivityBehaviorFactory) {
return configuration -> {
// 注册监听器例如说 BpmActivityEventListener
configuration.setEventListeners(ListUtil.toList(listeners.iterator()));
// 设置 ActivityBehaviorFactory 实现类用于流程任务的审核人的自定义
configuration.setActivityBehaviorFactory(bpmActivityBehaviorFactory);
// 设置自定义的函数
configuration.setCustomFlowableFunctionDelegates(ListUtil.toList(customFlowableFunctionDelegates.stream().iterator()));
};
}

View File

@ -1,7 +1,9 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
import lombok.Setter;
import org.flowable.bpmn.model.Activity;
import org.flowable.engine.delegate.DelegateExecution;
@ -48,12 +50,17 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav
super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId());
// 第二步获取任务的所有处理人
// 由于每次审批会签或签等情况后都会执行一次所以 variable 已经有结果不重复计算
@SuppressWarnings("unchecked")
Set<Long> assigneeUserIds = (Set<Long>) execution.getVariable(super.collectionVariable, Set.class);
if (assigneeUserIds == null) {
assigneeUserIds = taskCandidateInvoker.calculateUsers(execution);
execution.setVariable(super.collectionVariable, assigneeUserIds);
if (CollUtil.isEmpty(assigneeUserIds)) {
// 特殊如果没有处理人的情况下至少有一个 null 空元素避免自动通过
// 这样保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
// 用途1审批人为空时2审批类型为自动通过自动拒绝时
assigneeUserIds = SetUtils.asSet((Long) null);
}
}
return assigneeUserIds.size();
}

View File

@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import lombok.Setter;
@ -41,12 +43,17 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId());
// 第二步获取任务的所有处理人
// 由于每次审批会签或签等情况后都会执行一次所以 variable 已经有结果不重复计算
@SuppressWarnings("unchecked")
Set<Long> assigneeUserIds = (Set<Long>) execution.getVariable(super.collectionVariable, Set.class);
if (assigneeUserIds == null) {
assigneeUserIds = taskCandidateInvoker.calculateUsers(execution);
execution.setVariable(super.collectionVariable, assigneeUserIds);
if (CollUtil.isEmpty(assigneeUserIds)) {
// 特殊如果没有处理人的情况下至少有一个 null 空元素避免自动通过
// 这样保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
// 用途1审批人为空时2审批类型为自动通过自动拒绝时
assigneeUserIds = SetUtils.asSet((Long) null);
}
}
return assigneeUserIds.size();
}

View File

@ -1,7 +1,6 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.RandomUtil;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
import lombok.Setter;
@ -14,6 +13,7 @@ import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl;
import org.flowable.engine.impl.util.TaskHelper;
import org.flowable.task.service.TaskService;
import org.flowable.task.service.impl.persistence.entity.TaskEntity;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Set;
@ -36,14 +36,16 @@ public class BpmUserTaskActivityBehavior extends UserTaskActivityBehavior {
}
@Override
@Transactional(rollbackFor = Exception.class)
protected void handleAssignments(TaskService taskService, String assignee, String owner,
List<String> candidateUsers, List<String> candidateGroups, TaskEntity task, ExpressionManager expressionManager,
DelegateExecution execution, ProcessEngineConfigurationImpl processEngineConfiguration) {
// 第一步获得任务的候选用户
Long assigneeUserId = calculateTaskCandidateUsers(execution);
Assert.notNull(assigneeUserId, "任务处理人不能为空");
// 第二步设置作为负责人
TaskHelper.changeTaskAssignee(task, String.valueOf(assigneeUserId));
if (assigneeUserId != null) {
TaskHelper.changeTaskAssignee(task, String.valueOf(assigneeUserId));
}
}
private Long calculateTaskCandidateUsers(DelegateExecution execution) {
@ -56,6 +58,9 @@ public class BpmUserTaskActivityBehavior extends UserTaskActivityBehavior {
// 情况二如果非多实例的任务则计算任务处理人
// 第一步先计算可处理该任务的处理人们
Set<Long> candidateUserIds = taskCandidateInvoker.calculateUsers(execution);
if (CollUtil.isEmpty(candidateUserIds)) {
return null;
}
// 第二步后随机选择一个任务的处理人
// 疑问为什么一定要选择一个任务处理人
// 解答项目对 bpm 的任务是责任到人所以每个任务有且仅有一个处理人

View File

@ -2,11 +2,17 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignStartUserHandlerTypeEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import com.google.common.annotations.VisibleForTesting;
@ -14,15 +20,12 @@ import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.UserTask;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.runtime.ProcessInstance;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.MODEL_DEPLOY_FAIL_TASK_CANDIDATE_NOT_CONFIG;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.TASK_CREATE_FAIL_NO_CANDIDATE_USER;
/**
* {@link BpmTaskCandidateStrategy} 的调用者用于调用对应的策略实现任务的候选人的计算
@ -57,7 +60,14 @@ public class BpmTaskCandidateInvoker {
List<UserTask> userTaskList = BpmnModelUtils.getBpmnModelElements(bpmnModel, UserTask.class);
// 遍历所有的 UserTask校验审批人配置
userTaskList.forEach(userTask -> {
// 1. 非空校验
// 1.1 非人工审批无需校验审批人配置
Integer approveType = BpmnModelUtils.parseApproveType(userTask);
if (ObjectUtils.equalsAny(approveType,
BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType(),
BpmUserTaskApproveTypeEnum.AUTO_REJECT.getType())) {
return;
}
// 1.2 非空校验
Integer strategy = BpmnModelUtils.parseCandidateStrategy(userTask);
String param = BpmnModelUtils.parseCandidateParam(userTask);
if (strategy == null) {
@ -80,19 +90,31 @@ public class BpmTaskCandidateInvoker {
*/
@DataPermission(enable = false) // 忽略数据权限避免因为过滤导致找不到候选人
public Set<Long> calculateUsers(DelegateExecution execution) {
// 审批类型非人工审核时不进行计算候选人原因是后续会自动通过不通过
Integer approveType = BpmnModelUtils.parseApproveType(execution.getCurrentFlowElement());
if (ObjectUtils.equalsAny(approveType,
BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType(),
BpmUserTaskApproveTypeEnum.AUTO_REJECT.getType())) {
return new HashSet<>();
}
Integer strategy = BpmnModelUtils.parseCandidateStrategy(execution.getCurrentFlowElement());
String param = BpmnModelUtils.parseCandidateParam(execution.getCurrentFlowElement());
// 1.1 计算任务的候选人
Set<Long> userIds = getCandidateStrategy(strategy).calculateUsers(execution, param);
removeDisableUsers(userIds);
// 1.2 移除被禁用的用户
removeDisableUsers(userIds);
// 2. 校验是否有候选人
// 2. 候选人为空时根据审批人为空的配置补充
if (CollUtil.isEmpty(userIds)) {
log.error("[calculateUsers][流程任务({}/{}/{}) 任务规则({}/{}) 找不到候选人]", execution.getId(),
execution.getProcessDefinitionId(), execution.getCurrentActivityId(), strategy, param);
throw exception(TASK_CREATE_FAIL_NO_CANDIDATE_USER);
userIds = getCandidateStrategy(BpmTaskCandidateStrategyEnum.ASSIGN_EMPTY.getStrategy())
.calculateUsers(execution, param);
// ASSIGN_EMPTY 策略不需要移除被禁用的用户原因是再移除可能会出现更没审批人了
}
// 3. 移除发起人的用户
removeStartUserIfSkip(execution, userIds);
return userIds;
}
@ -108,7 +130,30 @@ public class BpmTaskCandidateInvoker {
});
}
private BpmTaskCandidateStrategy getCandidateStrategy(Integer strategy) {
/**
* 如果审批人与发起人相同时配置了 SKIP 跳过则移除发起人
*
* 注意如果只有一个候选人则不处理避免无法审批
*
* @param execution 执行中的任务
* @param assigneeUserIds 当前分配的候选人
*/
@VisibleForTesting
void removeStartUserIfSkip(DelegateExecution execution, Set<Long> assigneeUserIds) {
if (CollUtil.size(assigneeUserIds) <= 1) {
return;
}
Integer assignStartUserHandlerType = BpmnModelUtils.parseAssignStartUserHandlerType(execution.getCurrentFlowElement());
if (ObjectUtil.notEqual(assignStartUserHandlerType, BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType())) {
return;
}
ProcessInstance processInstance = SpringUtil.getBean(BpmProcessInstanceService.class)
.getProcessInstance(execution.getProcessInstanceId());
Assert.notNull(processInstance, "流程实例({}) 不存在", execution.getProcessInstanceId());
assigneeUserIds.remove(Long.valueOf(processInstance.getStartUserId()));
}
public BpmTaskCandidateStrategy getCandidateStrategy(Integer strategy) {
BpmTaskCandidateStrategyEnum strategyEnum = BpmTaskCandidateStrategyEnum.valueOf(strategy);
Assert.notNull(strategyEnum, "策略(%s) 不存在", strategy);
BpmTaskCandidateStrategy strategyObj = strategyMap.get(strategyEnum);

View File

@ -2,12 +2,14 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.runtime.ProcessInstance;
import java.util.Collections;
import java.util.Set;
/**
* BPM 任务的候选人的策略接口
*
* <p>
* 例如说分配审批人
*
* @author 芋道源码
@ -28,14 +30,6 @@ public interface BpmTaskCandidateStrategy {
*/
void validateParam(String param);
/**
* 基于执行任务获得任务的候选用户们
*
* @param execution 执行任务
* @return 用户编号集合
*/
Set<Long> calculateUsers(DelegateExecution execution, String param);
/**
* 是否一定要输入参数
*
@ -45,4 +39,50 @@ public interface BpmTaskCandidateStrategy {
return true;
}
/**
* 基于候选人参数获得任务的候选用户们
*
* @param param 执行任务
* @return 用户编号集合
*/
default Set<Long> calculateUsers(String param) {
return Collections.emptySet();
}
/**
* 基于执行任务获得任务的候选用户们
*
* @param execution 执行任务
* @return 用户编号集合
*/
default Set<Long> calculateUsers(DelegateExecution execution, String param) {
Set<Long> users = calculateUsers(param);
removeDisableUsers(users);
return users;
}
/**
* 基于流程实例获得任务的候选用户们
* <p>
* 目的用于获取未执行节点的候选用户们
*
* @param startUserId 流程发起人编号
* @param processInstance 流程实例编号
* @param activityId 活动 Id (对应 Bpmn XML id)
* @param param 节点的参数
* @return 用户编号集合
*/
default Set<Long> calculateUsers(Long startUserId, ProcessInstance processInstance, String activityId, String param) {
Set<Long> users = calculateUsers(param);
removeDisableUsers(users);
return users;
}
/**
* 移除被禁用的用户
*
* @param users 用户 Ids
*/
void removeDisableUsers(Set<Long> users);
}

View File

@ -0,0 +1,78 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
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 java.util.*;
/**
* 部门的负责人 {@link BpmTaskCandidateStrategy} 抽象类
*
* @author jason
*/
public abstract class BpmTaskCandidateAbstractDeptLeaderStrategy extends BpmTaskCandidateAbstractStrategy {
protected DeptApi deptApi;
public BpmTaskCandidateAbstractDeptLeaderStrategy(AdminUserApi adminUserApi, DeptApi deptApi) {
super(adminUserApi);
this.deptApi = deptApi;
}
/**
* 获得指定层级的部门负责人只有第 level 的负责人
*
* @param dept 指定部门
* @param level 第几级
* @return 部门负责人的编号
*/
protected Long getAssignLevelDeptLeaderId(DeptRespDTO dept, Integer level) {
Assert.isTrue(level > 0, "level 必须大于 0");
if (dept == null) {
return null;
}
DeptRespDTO currentDept = dept;
for (int i = 1; i < level; i++) {
DeptRespDTO parentDept = deptApi.getDept(currentDept.getParentId());
if (parentDept == null) { // 找不到父级部门到了最高级返回最高级的部门负责人
break;
}
currentDept = parentDept;
}
return currentDept.getLeaderUserId();
}
/**
* 获得连续层级的部门负责人包含 [1, level] 的负责人
*
* @param deptIds 指定部门编号数组
* @param level 最大层级
* @return 连续部门负责人 Id
*/
protected Set<Long> getMultiLevelDeptLeaderIds(List<Long> deptIds, Integer level) {
Assert.isTrue(level > 0, "level 必须大于 0");
if (CollUtil.isEmpty(deptIds)) {
return new HashSet<>();
}
Set<Long> deptLeaderIds = new LinkedHashSet<>(); // 保证有序
for (Long deptId : deptIds) {
DeptRespDTO dept = deptApi.getDept(deptId);
for (int i = 0; i < level; i++) {
if (dept.getLeaderUserId() != null) {
deptLeaderIds.add(dept.getLeaderUserId());
}
DeptRespDTO parentDept = deptApi.getDept(dept.getParentId());
if (parentDept == null) { // 找不到父级部门. 已经到了最高层级了
break;
}
dept = parentDept;
}
}
return deptLeaderIds;
}
}

View File

@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import java.util.Map;
import java.util.Set;
/**
* {@link BpmTaskCandidateStrategy} 抽象类
*
* @author jason
*/
public abstract class BpmTaskCandidateAbstractStrategy implements BpmTaskCandidateStrategy {
protected AdminUserApi adminUserApi;
public BpmTaskCandidateAbstractStrategy(AdminUserApi adminUserApi) {
this.adminUserApi = adminUserApi;
}
@Override
public void removeDisableUsers(Set<Long> users) {
if (CollUtil.isEmpty(users)) {
return;
}
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(users);
users.removeIf(id -> {
AdminUserRespDTO user = userMap.get(id);
return user == null || !CommonStatusEnum.ENABLE.getStatus().equals(user.getStatus());
});
}
}

View File

@ -0,0 +1,66 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignEmptyHandlerTypeEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
import org.flowable.engine.delegate.DelegateExecution;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
/**
* 审批人为空 {@link BpmTaskCandidateStrategy} 实现类
*
* @author kyle
*/
@Component
public class BpmTaskCandidateAssignEmptyStrategy extends BpmTaskCandidateAbstractStrategy {
@Resource
@Lazy // 延迟加载避免循环依赖
private BpmProcessDefinitionService processDefinitionService;
public BpmTaskCandidateAssignEmptyStrategy(AdminUserApi adminUserApi) {
super(adminUserApi);
}
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
return BpmTaskCandidateStrategyEnum.ASSIGN_EMPTY;
}
@Override
public void validateParam(String param) {
}
@Override
public Set<Long> calculateUsers(DelegateExecution execution, String param) {
// 情况一指定人员审批
Integer assignEmptyHandlerType = BpmnModelUtils.parseAssignEmptyHandlerType(execution.getCurrentFlowElement());
if (Objects.equals(assignEmptyHandlerType, BpmUserTaskAssignEmptyHandlerTypeEnum.ASSIGN_USER.getType())) {
Set<Long> users = new HashSet<>(BpmnModelUtils.parseAssignEmptyHandlerUserIds(execution.getCurrentFlowElement()));
removeDisableUsers(users);
return users;
}
// 情况二流程管理员
if (Objects.equals(assignEmptyHandlerType, BpmUserTaskAssignEmptyHandlerTypeEnum.ASSIGN_ADMIN.getType())) {
BpmProcessDefinitionInfoDO processDefinition = processDefinitionService.getProcessDefinitionInfo(execution.getProcessDefinitionId());
Assert.notNull(processDefinition, "流程定义({})不存在", execution.getProcessDefinitionId());
return new HashSet<>(processDefinition.getManagerUserIds());
}
// 都不满足还是返回空
return new HashSet<>();
}
}

View File

@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.util.string.StrUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import org.springframework.stereotype.Component;
import java.util.Set;
/**
* 连续多级部门的负责人 {@link BpmTaskCandidateStrategy} 实现类
*
* @author jason
*/
@Component
public class BpmTaskCandidateDeptLeaderMultiStrategy extends BpmTaskCandidateAbstractDeptLeaderStrategy {
public BpmTaskCandidateDeptLeaderMultiStrategy(AdminUserApi adminUserApi, DeptApi deptApi) {
super(adminUserApi, deptApi);
}
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
return BpmTaskCandidateStrategyEnum.MULTI_DEPT_LEADER_MULTI;
}
@Override
public void validateParam(String param) {
// 参数格式: | 分隔1左边为部门多个部门用 , 分隔2右边为部门层级
String[] params = param.split("\\|");
Assert.isTrue(params.length == 2, "参数格式不匹配");
deptApi.validateDeptList(StrUtils.splitToLong(params[0], ","));
Assert.isTrue(Integer.parseInt(params[1]) > 0, "部门层级必须大于 0");
}
@Override
public Set<Long> calculateUsers(String param) {
String[] params = param.split("\\|");
return getMultiLevelDeptLeaderIds(StrUtils.splitToLong(params[0], ","), Integer.valueOf(params[1]));
}
}

View File

@ -5,10 +5,9 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCand
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import org.flowable.engine.delegate.DelegateExecution;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.Set;
@ -20,10 +19,14 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
* @author kyle
*/
@Component
public class BpmTaskCandidateDeptLeaderStrategy implements BpmTaskCandidateStrategy {
public class BpmTaskCandidateDeptLeaderStrategy extends BpmTaskCandidateAbstractStrategy {
@Resource
private DeptApi deptApi;
private final DeptApi deptApi;
public BpmTaskCandidateDeptLeaderStrategy(AdminUserApi adminUserApi, DeptApi deptApi) {
super(adminUserApi);
this.deptApi = deptApi;
}
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
@ -37,7 +40,7 @@ public class BpmTaskCandidateDeptLeaderStrategy implements BpmTaskCandidateStrat
}
@Override
public Set<Long> calculateUsers(DelegateExecution execution, String param) {
public Set<Long> calculateUsers(String param) {
Set<Long> deptIds = StrUtils.splitToLongSet(param);
List<DeptRespDTO> depts = deptApi.getDeptList(deptIds);
return convertSet(depts, DeptRespDTO::getLeaderUserId);

View File

@ -6,10 +6,8 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidat
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
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.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.Set;
@ -21,12 +19,14 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
* @author kyle
*/
@Component
public class BpmTaskCandidateDeptMemberStrategy implements BpmTaskCandidateStrategy {
public class BpmTaskCandidateDeptMemberStrategy extends BpmTaskCandidateAbstractStrategy {
@Resource
private DeptApi deptApi;
@Resource
private AdminUserApi adminUserApi;
private final DeptApi deptApi;
public BpmTaskCandidateDeptMemberStrategy(AdminUserApi adminUserApi, DeptApi deptApi) {
super(adminUserApi);
this.deptApi = deptApi;
}
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
@ -40,7 +40,7 @@ public class BpmTaskCandidateDeptMemberStrategy implements BpmTaskCandidateStrat
}
@Override
public Set<Long> calculateUsers(DelegateExecution execution, String param) {
public Set<Long> calculateUsers(String param) {
Set<Long> deptIds = StrUtils.splitToLongSet(param);
List<AdminUserRespDTO> users = adminUserApi.getUserListByDeptIds(deptIds);
return convertSet(users, AdminUserRespDTO::getId);

View File

@ -4,6 +4,7 @@ import cn.hutool.core.convert.Convert;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import org.flowable.engine.delegate.DelegateExecution;
import org.springframework.stereotype.Component;
@ -15,7 +16,11 @@ import java.util.Set;
* @author 芋道源码
*/
@Component
public class BpmTaskCandidateExpressionStrategy implements BpmTaskCandidateStrategy {
public class BpmTaskCandidateExpressionStrategy extends BpmTaskCandidateAbstractStrategy {
public BpmTaskCandidateExpressionStrategy(AdminUserApi adminUserApi) {
super(adminUserApi);
}
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
@ -30,7 +35,9 @@ public class BpmTaskCandidateExpressionStrategy implements BpmTaskCandidateStrat
@Override
public Set<Long> calculateUsers(DelegateExecution execution, String param) {
Object result = FlowableUtils.getExpressionValue(execution, param);
return Convert.toSet(Long.class, result);
Set<Long> users = Convert.toSet(Long.class, result);
removeDisableUsers(users);
return users;
}
}

View File

@ -5,10 +5,9 @@ import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmUserGroupDO;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import cn.iocoder.yudao.module.bpm.service.definition.BpmUserGroupService;
import org.flowable.engine.delegate.DelegateExecution;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;
import java.util.Set;
@ -21,10 +20,14 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
* @author kyle
*/
@Component
public class BpmTaskCandidateGroupStrategy implements BpmTaskCandidateStrategy {
public class BpmTaskCandidateGroupStrategy extends BpmTaskCandidateAbstractStrategy {
@Resource
private BpmUserGroupService userGroupService;
private final BpmUserGroupService userGroupService;
public BpmTaskCandidateGroupStrategy(AdminUserApi adminUserApi, BpmUserGroupService userGroupService) {
super(adminUserApi);
this.userGroupService = userGroupService;
}
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
@ -38,7 +41,7 @@ public class BpmTaskCandidateGroupStrategy implements BpmTaskCandidateStrategy {
}
@Override
public Set<Long> calculateUsers(DelegateExecution execution, String param) {
public Set<Long> calculateUsers(String param) {
Set<Long> groupIds = StrUtils.splitToLongSet(param);
List<BpmUserGroupDO> groups = userGroupService.getUserGroupList(groupIds);
return convertSetByFlatMap(groups, BpmUserGroupDO::getUserIds, Collection::stream);

View File

@ -6,10 +6,8 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidat
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 org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.Set;
@ -21,12 +19,14 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
* @author kyle
*/
@Component
public class BpmTaskCandidatePostStrategy implements BpmTaskCandidateStrategy {
public class BpmTaskCandidatePostStrategy extends BpmTaskCandidateAbstractStrategy {
@Resource
private PostApi postApi;
@Resource
private AdminUserApi adminUserApi;
private final PostApi postApi;
public BpmTaskCandidatePostStrategy(AdminUserApi adminUserApi, PostApi postApi) {
super(adminUserApi);
this.postApi = postApi;
}
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
@ -40,7 +40,7 @@ public class BpmTaskCandidatePostStrategy implements BpmTaskCandidateStrategy {
}
@Override
public Set<Long> calculateUsers(DelegateExecution execution, String param) {
public Set<Long> calculateUsers(String param) {
Set<Long> postIds = StrUtils.splitToLongSet(param);
List<AdminUserRespDTO> users = adminUserApi.getUserListByPostIds(postIds);
return convertSet(users, AdminUserRespDTO::getId);

View File

@ -5,10 +5,10 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCand
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
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 cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Set;
/**
@ -17,13 +17,17 @@ import java.util.Set;
* @author kyle
*/
@Component
public class BpmTaskCandidateRoleStrategy implements BpmTaskCandidateStrategy {
public class BpmTaskCandidateRoleStrategy extends BpmTaskCandidateAbstractStrategy {
@Resource
private RoleApi roleApi;
@Resource
private PermissionApi permissionApi;
public BpmTaskCandidateRoleStrategy(AdminUserApi adminUserApi) {
super(adminUserApi);
}
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
return BpmTaskCandidateStrategyEnum.ROLE;
@ -36,7 +40,7 @@ public class BpmTaskCandidateRoleStrategy implements BpmTaskCandidateStrategy {
}
@Override
public Set<Long> calculateUsers(DelegateExecution execution, String param) {
public Set<Long> calculateUsers(String param) {
Set<Long> roleIds = StrUtils.splitToLongSet(param);
return permissionApi.getUserRoleIdListByRoleIds(roleIds);
}

View File

@ -0,0 +1,90 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.runtime.ProcessInstance;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import java.util.HashSet;
import java.util.Set;
import static cn.hutool.core.collection.ListUtil.toList;
/**
* 发起人连续多级部门的负责人 {@link BpmTaskCandidateStrategy} 实现类
*
* @author jason
*/
@Component
public class BpmTaskCandidateStartUserDeptLeaderMultiStrategy extends BpmTaskCandidateAbstractDeptLeaderStrategy {
@Resource
@Lazy
private BpmProcessInstanceService processInstanceService;
public BpmTaskCandidateStartUserDeptLeaderMultiStrategy(AdminUserApi adminUserApi, DeptApi deptApi) {
super(adminUserApi, deptApi);
}
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
return BpmTaskCandidateStrategyEnum.START_USER_DEPT_LEADER_MULTI;
}
@Override
public void validateParam(String param) {
// 参数是部门的层级
Assert.isTrue(Integer.parseInt(param) > 0, "部门的层级必须大于 0");
}
@Override
public Set<Long> calculateUsers(DelegateExecution execution, String param) {
// 获得流程发起人
ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId());
Long startUserId = NumberUtils.parseLong(processInstance.getStartUserId());
// 获取发起人的 multi 部门负责人
DeptRespDTO dept = getStartUserDept(startUserId);
if (dept == null) {
return new HashSet<>();
}
Set<Long> users = getMultiLevelDeptLeaderIds(toList(dept.getId()), Integer.valueOf(param)); // 参数是部门的层级
// TODO @jason这里 removeDisableUsers 的原因是啥呀
removeDisableUsers(users);
return users;
}
@Override
public Set<Long> calculateUsers(Long startUserId, ProcessInstance processInstance, String activityId, String param) {
DeptRespDTO dept = getStartUserDept(startUserId);
if (dept == null) {
return new HashSet<>();
}
Set<Long> users = getMultiLevelDeptLeaderIds(toList(dept.getId()), Integer.valueOf(param)); // 参数是部门的层级
removeDisableUsers(users);
return users;
}
/**
* 获取发起人的部门
*
* @param startUserId 发起人 Id
*/
protected DeptRespDTO getStartUserDept(Long startUserId) {
AdminUserRespDTO startUser = adminUserApi.getUser(startUserId);
if (startUser.getDeptId() == null) { // 找不到部门
return null;
}
return deptApi.getDept(startUser.getDeptId());
}
}

View File

@ -0,0 +1,91 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.runtime.ProcessInstance;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import java.util.HashSet;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
/**
* 发起人的部门负责人, 可以是上级部门负责人 {@link BpmTaskCandidateStrategy} 实现类
*
* @author jason
*/
@Component
public class BpmTaskCandidateStartUserDeptLeaderStrategy extends BpmTaskCandidateAbstractDeptLeaderStrategy {
@Resource
@Lazy // 避免循环依赖
private BpmProcessInstanceService processInstanceService;
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
return BpmTaskCandidateStrategyEnum.START_USER_DEPT_LEADER;
}
public BpmTaskCandidateStartUserDeptLeaderStrategy(AdminUserApi adminUserApi, DeptApi deptApi) {
super(adminUserApi, deptApi);
}
@Override
public void validateParam(String param) {
// 参数是部门的层级
Assert.isTrue(Integer.parseInt(param) > 0, "部门的层级必须大于 0");
}
@Override
public Set<Long> calculateUsers(DelegateExecution execution, String param) {
// 获得流程发起人
ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId());
Long startUserId = NumberUtils.parseLong(processInstance.getStartUserId());
// 获取发起人的部门负责人
Set<Long> users = getStartUserDeptLeader(startUserId, param);
removeDisableUsers(users);
return users;
}
@Override
public Set<Long> calculateUsers(Long startUserId, ProcessInstance processInstance, String activityId, String param) {
// 获取发起人的部门负责人
Set<Long> users = getStartUserDeptLeader(startUserId, param);
removeDisableUsers(users);
return users;
}
private Set<Long> getStartUserDeptLeader(Long startUserId, String param) {
DeptRespDTO dept = getStartUserDept(startUserId);
if (dept == null) {
return new HashSet<>();
}
Long deptLeaderId = getAssignLevelDeptLeaderId(dept, Integer.valueOf(param)); // 参数是部门的层级
return deptLeaderId != null ? asSet(deptLeaderId) : new HashSet<>();
}
/**
* 获取发起人的部门
*
* @param startUserId 发起人 Id
*/
protected DeptRespDTO getStartUserDept(Long startUserId) {
AdminUserRespDTO startUser = adminUserApi.getUser(startUserId);
if (startUser.getDeptId() == null) { // 找不到部门
return null;
}
return deptApi.getDept(startUser.getDeptId());
}
}

View File

@ -2,11 +2,12 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.UserTask;
import org.flowable.engine.delegate.DelegateExecution;
@ -14,7 +15,6 @@ import org.flowable.engine.runtime.ProcessInstance;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.*;
/**
@ -23,12 +23,16 @@ import java.util.*;
* @author 芋道源码
*/
@Component
public class BpmTaskCandidateStartUserSelectStrategy implements BpmTaskCandidateStrategy {
public class BpmTaskCandidateStartUserSelectStrategy extends BpmTaskCandidateAbstractStrategy {
@Resource
@Lazy // 延迟加载避免循环依赖
private BpmProcessInstanceService processInstanceService;
public BpmTaskCandidateStartUserSelectStrategy(AdminUserApi adminUserApi) {
super(adminUserApi);
}
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
return BpmTaskCandidateStrategyEnum.START_USER_SELECT;
@ -46,7 +50,23 @@ public class BpmTaskCandidateStartUserSelectStrategy implements BpmTaskCandidate
execution.getProcessInstanceId());
// 获得审批人
List<Long> assignees = startUserSelectAssignees.get(execution.getCurrentActivityId());
return new LinkedHashSet<>(assignees);
Set<Long> users = new LinkedHashSet<>(assignees);
removeDisableUsers(users);
return users;
}
@Override
public Set<Long> calculateUsers(Long startUserId, ProcessInstance processInstance, String activityId, String param) {
if (processInstance == null) {
return Collections.emptySet();
}
Map<String, List<Long>> startUserSelectAssignees = FlowableUtils.getStartUserSelectAssignees(processInstance);
Assert.notNull(startUserSelectAssignees, "流程实例({}) 的发起人自选审批人不能为空", processInstance.getId());
// 获得审批人
List<Long> assignees = startUserSelectAssignees.get(activityId);
Set<Long> users = new LinkedHashSet<>(assignees);
removeDisableUsers(users);
return users;
}
@Override

Some files were not shown because too many files have changed in this diff Show More