feat(flow): 新增表达式分配用户策略支持

- 在Node DTO中添加assignExpress和assignExpressName字段
- 实现AssignExpressStrategyImpl表达式分配策略类
- 支持通过表达式动态计算任务执行人列表
- 优化NodeFormatUtil中的用户分配逻辑
- 更新ProcessInstanceConstant添加表达式分配类型常量
This commit is contained in:
lengleng authored and pigcloud committed 2025-10-09 14:48:42 +08:00
1 parent 79b27b6d7d
commit 0eaa4b6d11
6 files changed
+1069 -14

No files matched your search

+324
View File
@@ -0,0 +1,324 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
PigX is a proprietary enterprise-grade microservices platform built on Spring Cloud Alibaba, focusing on AI knowledge
management, document processing, and intelligent Q&A capabilities. The current version is 5.10.2-SNAPSHOT.
**Important**: This is NOT an open-source project. All rights reserved by the author. Commercial use requires proper
licensing.
## Technology Stack
- **Java Version**: Java 17
- **Spring Boot**: 3.5.6
- **Spring Cloud**: 2025.0.0
- **Spring Cloud Alibaba**: 2023.0.3.3
- **AI/ML Libraries**:
- LangChain4j: 1.6.0
- Spring AI: 1.0.2
- **Database**:
- MySQL (for business data)
- PostgreSQL with pgvector extension (for vector storage)
- Support for Qdrant, Milvus, Chroma, Neo4j
- **ORM**: MyBatis-Plus
- **Container**: Undertow
- **Service Discovery**: Nacos
- **Build Tool**: Maven
## Project Structure
### Module Organization
```
pigx/
├── pigx-boot/ # Monolithic deployment starter (combines all modules)
├── pigx-knowledge/ # AI knowledge base module (core AI capabilities)
├── pigx-auth/ # Authentication module (dependency)
├── pigx-upms-biz/ # User management module (dependency)
├── pigx-gateway/ # API gateway (microservices mode)
├── pigx-register/ # Nacos registry (microservices mode)
├── pigx-visual/ # Visual management modules (monitoring, codegen, etc.)
├── db/ # Database initialization scripts
└── docker-compose.yml # Docker compose configuration
```
### Key Module: pigx-knowledge
The knowledge module follows a standard layered architecture:
```
pigx-knowledge/src/main/java/com/pig4cloud/pigx/knowledge/
├── controller/ # REST API endpoints
├── service/ # Business logic layer
│ └── impl/ # Service implementations
├── mapper/ # MyBatis mapper interfaces
├── entity/ # JPA/MyBatis entities
├── dto/ # Data transfer objects
├── config/ # Spring configuration classes
└── support/ # Support utilities and handlers
├── constant/ # Enums and constants
├── handler/ # Specialized handlers
│ ├── rag/ # RAG (Retrieval Augmented Generation) handlers
│ ├── parse/ # Document parsing handlers
│ ├── model/ # AI model handlers
│ └── websearch/ # Web search handlers
├── provider/ # External service providers
├── rule/ # LiteFlow rule definitions
├── function/ # LangChain4j functions
└── util/ # Utility classes
```
### Vector Storage Architecture
The system uses a factory pattern for vector store implementations:
- **Interface**: `EmbeddingStoreFactory` (pigx-knowledge/support/handler/rag/)
- **Supported Stores**: Qdrant, Milvus, Chroma, PGVector, Neo4j
- **Factory Method**: `createEmbeddingStore(embedStoreEntity, datasetEntity)`
- Each store type has its own factory implementation that checks `support(storeType)`
## Build and Run Commands
### Maven Profiles
The project has two main profiles:
- **cloud** (default): Microservices mode with Spring Cloud
- **boot**: Monolithic mode using pigx-boot
### Build Commands
```bash
# Build entire project (microservices mode)
mvn clean install
# Build monolithic version
mvn clean install -P boot
# Build specific module
mvn clean install -pl pigx-knowledge
# Skip tests
mvn clean install -DskipTests
# Build Docker image for a module
mvn clean install docker:build -pl pigx-knowledge
# Format code according to Spring Java Format
mvn spring-javaformat:apply
```
### Run Commands
#### Monolithic Mode (Recommended for Development)
```bash
# Run pigx-boot module
cd pigx-boot
mvn spring-boot:run
# Or run with specific profile
mvn spring-boot:run -Dspring-boot.run.profiles=dev
```
**Default port**: 8080 (configured in pigx-boot)
#### Microservices Mode
```bash
# Start all services using Docker Compose
docker-compose up
# Or start infrastructure first, then individual services
# 1. Start Nacos (service discovery)
cd pigx-register && mvn spring-boot:run
# 2. Start gateway
cd pigx-gateway && mvn spring-boot:run
# 3. Start knowledge service
cd pigx-knowledge && mvn spring-boot:run
```
**Knowledge service port**: 9010
### Test Commands
```bash
# Run all tests
mvn test
# Run tests for specific module
mvn test -pl pigx-knowledge
# Run specific test class
mvn test -pl pigx-knowledge -Dtest=YourTestClass
# Run tests with coverage
mvn clean test jacoco:report
```
## Development Configuration
### Database Setup
The project uses PostgreSQL with pgvector for the boot module:
- **Host**: localhost (or pgvector in Docker)
- **Port**: 5432
- **Database**: pigxx_boot
- **Username**: postgres
- **Password**: postgres
Database initialization scripts are in `db/` directory:
- `0pigxx_boot.sql` - Boot module schema
- `9pigxx_ai.sql` - AI knowledge base schema
### Redis Configuration
- **Host**: 127.0.0.1
- **Database**: 5
### Nacos Configuration
For microservices mode:
- **Server**: pigx-register:8848
- **Username**: nacos
- **Password**: nacos
- Configuration files are loaded from Nacos dynamically
### File Storage
Local file storage (configurable):
- **Path**: /Users/lengleng/Downloads/img (development)
- Can be configured in `application-dev.yml`
## Code Style and Conventions
### Code Formatting
The project uses Spring Java Format. Run before committing:
```bash
mvn spring-javaformat:apply
```
### Package Naming Conventions
- **Controllers**: Suffix with `Controller` (e.g., `AiDatasetController`)
- **Services**: Interface without suffix, impl with `Impl` (e.g., `EmbeddingStoreService`, `EmbeddingStoreServiceImpl`)
- **Mappers**: Suffix with `Mapper` for both interface and XML (e.g., `AiDatasetMapper.java`, `AiDatasetMapper.xml`)
- **Entities**: Suffix with `Entity` (e.g., `AiDatasetEntity`)
- **DTOs**: Suffix with `DTO` (e.g., `AiMultimodalEmbeddingArkDTO`)
- **Enums**: Suffix with `Enums` (e.g., `EmbedBizTypeEnums`, `ModelSupportEnums`)
### MyBatis Mapper XML Location
All MyBatis mapper XMLs are in: `src/main/resources/mapper/*.xml`
### Constants and Enums
Located in `support/constant/` package. Key enums:
- `EmbedBizTypeEnums` - Embedding business types
- `ModelSupportEnums` - Supported AI models
- `EmbedStoreSupportEnums` - Supported vector stores
- `FileTypeEnums` - Supported file types
- `ChatTypeEnums` - Chat interaction types
## AI/RAG Implementation Details
### Embedding Store Factory Pattern
When working with vector stores:
1. Create a factory class implementing `EmbeddingStoreFactory`
2. Implement `support(String storeType)` to declare supported types
3. Implement `createEmbeddingStore(...)` to instantiate the store
4. Register as a Spring `@Component`
### LangChain4j Integration
- Document parsers: Apache PDFBox, Apache POI
- Embedding models: Support for OpenAI, custom models via factory
- Vector stores: Qdrant, Milvus, Chroma, PGVector
- Document splitters and transformers in handler/rag package
### LiteFlow Rule Engine
Business rules are defined in the `support/rule/` package using LiteFlow DSL.
## API Documentation
API documentation is available via Swagger/Knife4j:
- **Swagger UI**: http://localhost:9010/doc.html (knowledge service)
- **OpenAPI**: Enabled via `@EnableOpenApi` annotation
- Token URL for auth: Configured in swagger.token-url property
## Git Workflow
**Current branch**: ai-leng-dev
**Main branch**: dev (use this for PRs)
## Important Notes
### Security
- Configuration encryption: Uses Jasypt with password "pigx"
- Login encryption: Uses AES with key "pigxpigxpigxpigx" (16 chars)
- OAuth2 resource server protection enabled
### Docker
Docker Maven plugin is configured but disabled by default (`docker.skip=false` in pom).
Registry: registry.cn-hangzhou.aliyuncs.com/pigcloudx
### Magic API
Low-code API platform is integrated for dynamic API creation:
- Configuration: `src/main/resources/lowcode/magic-api.yml`
- Requires additional setup for production use
### File Parsing
Supports multiple formats via Tika, Torchv, and custom parsers:
- PDF, Word, Excel, PowerPoint (Apache POI)
- HTML to DOCX conversion (Aspose Words)
- Unstructured data (Torchv)
## Common Development Tasks
### Adding a New Vector Store
1. Create factory class in `pigx-knowledge/support/handler/rag/`
2. Implement `EmbeddingStoreFactory` interface
3. Add store type to `EmbedStoreSupportEnums`
4. Add dependencies in `pigx-knowledge/pom.xml`
### Adding a New AI Model
1. Add model configuration to `ModelSupportEnums`
2. Create model provider in `support/provider/` if needed
3. Update embedding service implementation
### Adding a New Document Parser
1. Create parser handler in `support/handler/parse/`
2. Register file type in `FileTypeEnums`
3. Integrate with document processing pipeline
### Modifying Database Schema
1. Update SQL in `db/` directory
2. Update corresponding entity classes
3. Update MyBatis mapper interfaces and XMLs
4. Run database migration scripts
@@ -0,0 +1,130 @@
package com.pig4cloud.pigx.flow.constant;
/**
* 流程实例常量定义
* <p>
* 定义流程实例执行过程中使用的各种常量,包括: - 无执行人处理策略 - 拒绝处理策略 - 多人审批模式 - 任务分配类型 - 表单权限
* </p>
*
* @author pigx
*/
public class ProcessInstanceConstant {
/**
* 空执行人默认ID 当节点没有找到合适的执行人时使用该默认值
*/
public static final Long DEFAULT_EMPTY_ASSIGN = -99999999L;
/**
* 无执行人处理策略:自动通过 当任务节点没有找到执行人时,自动通过该节点
*/
public static final String USER_TASK_NOBODY_HANDLER_TO_PASS = "TO_PASS";
/**
* 无执行人处理策略:转交给管理员 当任务节点没有找到执行人时,将任务转交给流程管理员处理
*/
public static final String USER_TASK_NOBODY_HANDLER_TO_ADMIN = "TO_ADMIN";
/**
* 无执行人处理策略:指定人员 当任务节点没有找到执行人时,转交给预先指定的人员处理
*/
public static final String USER_TASK_NOBODY_HANDLER_TO_USER = "TO_USER";
/**
* 无执行人处理策略:结束流程 当任务节点没有找到执行人时,直接结束整个流程
*/
public static final String USER_TASK_NOBODY_HANDLER_TO_END = "TO_END";
/**
* 无执行人处理策略:拒绝 当任务节点没有找到执行人时,自动拒绝该任务
*/
public static final String USER_TASK_NOBODY_HANDLER_TO_REFUSE = "TO_REFUSE";
/**
* 拒绝处理策略:结束流程 审批人拒绝后,直接结束整个流程
*/
public static final String USER_TASK_REFUSE_TYPE_TO_END = "TO_END";
/**
* 拒绝处理策略:跳转到指定节点 审批人拒绝后,流程跳转到指定的节点继续执行
*/
public static final String USER_TASK_REFUSE_TYPE_TO_NODE = "TO_NODE";
/**
* 多人审批模式:会签 所有审批人都必须同意才能通过,任何一人拒绝则不通过
*/
public static final int MULTIPLE_MODE_AL_SAME = 1;
/**
* 多人审批模式:或签 只要有一个人同意即可通过,无需等待其他人审批
*/
public static final int MULTIPLE_MODE_ONE = 2;
/**
* 多人审批模式:顺签 按照指定顺序依次审批,前一人通过后才能轮到下一人
*/
public static final int MULTIPLE_MODE_ALL_SORT = 3;
/**
* 任务分配类型 定义审批节点的执行人分配方式
*/
public static class AssignedTypeClass {
/**
* 指定用户 直接指定具体的用户作为执行人
*/
public static final int USER = 1;
/**
* 部门主管 根据发起人或上一节点执行人的部门,找到对应的部门主管作为执行人
*/
public static final int LEADER = 2;
/**
* 发起人自己 由流程发起人自己处理该节点
*/
public static final int SELF = 5;
/**
* 表单人员 从表单中的用户选择字段获取执行人
*/
public static final int FORM_USER = 8;
/**
* 发起人自选 发起流程时由发起人手动选择该节点的执行人
*/
public static final int SELF_SELECT = 4;
/**
* 角色 指定拥有特定角色的所有用户作为执行人
*/
public static final int ROLE = 3;
/** 表达式常量值 */
public static final int EXPRESSION = 10;
}
/**
* 表单权限类型 定义节点对表单字段的操作权限
*/
public static class FormPermClass {
/**
* 隐藏 该字段对当前节点执行人不可见
*/
public static final String HIDE = "H";
/**
* 只读 该字段对当前节点执行人可见但不可编辑
*/
public static final String READ = "R";
/**
* 编辑 该字段对当前节点执行人可见且可编辑
*/
public static final String EDIT = "E";
}
}
@@ -0,0 +1,144 @@
package com.pig4cloud.pigx.flow.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* 流程节点数据传输对象
* <p>
* 用于流程定义的节点配置信息传输。该对象包含了节点的所有配置信息, 包括节点类型、审批人配置、表单权限、条件配置等。
* 支持嵌套结构,可以表示复杂的流程结构(如条件分支、并行分支)。
* </p>
*
* @author pigx
*/
@Data
public class Node {
/**
* 节点唯一标识
*/
private String id;
/**
* 父节点ID 用于构建节点的层级关系
*/
private String parentId;
/**
* 头节点ID 用于条件分支和并行分支的开始节点标识
*/
private String headId;
/**
* 尾节点ID 用于条件分支和并行分支的结束节点标识
*/
private String tailId;
/**
* 占位符文本 用于流程设计器中的显示
*/
private String placeHolder;
/**
* 节点类型 对应NodeTypeEnum中的值
*/
private Integer type;
/**
* 节点名称 节点的显示名称
*/
@JsonProperty(value = "nodeName")
private String name;
/**
* 错误标识 标识节点配置是否有错误
*/
private Boolean error;
/**
* 子节点 当前节点的下一个节点,用于构建节点链
*/
@JsonProperty("childNode")
private Node children;
/**
* 事件配置 JSON格式存储节点事件配置(如超时提醒、自动审批等)
*/
private String eventConfig;
/**
* 执行人分配类型 对应AssignedTypeClass中的常量(指定用户、部门主管、角色等)
*/
private Integer assignedType;
/**
* 是否多人审批 true=多人审批,false=单人审批
*/
private Boolean multiple;
/**
* 多人审批模式 1=会签(所有人都同意),2=或签(一人同意即可),3=顺签(依次审批)
*/
private Integer multipleMode;
/**
* 部门主管级别 当assignedType为LEADER时,指定查找第几级主管
*/
private Integer deptLeaderLevel;
/** 分配规则名称 */
private String assignExpressName;
/** 分配表达式 */
private String assignExpress;
/**
* 表单用户字段ID 当assignedType为FORM_USER时,指定从哪个表单字段获取用户
*/
private String formUserId;
/**
* 表单用户字段名称 用于显示
*/
private String formUserName;
/**
* 节点执行人列表 当assignedType为USER或ROLE时,存储具体的用户或角色信息
*/
private List<NodeUser> nodeUserList;
/**
* 条件节点列表 当节点类型为条件分支时,存储所有分支节点
*/
private List<Node> conditionNodes;
/**
* 表单字段权限配置 key=表单字段ID,value=权限类型(H=隐藏,R=只读,E=编辑)
*/
private Map<String, String> formPerms;
/**
* 无执行人处理配置 定义当节点找不到执行人时的处理策略
*/
private Nobody nobody;
/**
* 分组模式标识 用于条件配置的分组模式
*/
private Boolean groupMode;
/**
* 条件列表 当节点为条件分支时,定义各分支的执行条件
*/
private List<GroupCondition> conditionList;
/**
* 拒绝配置 定义节点拒绝后的处理策略
*/
private Refuse refuse;
}
@@ -0,0 +1,346 @@
package com.pig4cloud.pigx.flow.support.utils;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.pig4cloud.pigx.admin.api.entity.SysUser;
import com.pig4cloud.pigx.admin.api.feign.RemoteUserService;
import com.pig4cloud.pigx.common.core.util.RetOps;
import com.pig4cloud.pigx.common.security.util.SecurityUtils;
import com.pig4cloud.pigx.flow.constant.NodeStatusEnum;
import com.pig4cloud.pigx.flow.constant.NodeTypeEnum;
import com.pig4cloud.pigx.flow.constant.NodeUserTypeEnum;
import com.pig4cloud.pigx.flow.constant.ProcessInstanceConstant;
import com.pig4cloud.pigx.flow.dto.Node;
import com.pig4cloud.pigx.flow.dto.NodeUser;
import com.pig4cloud.pigx.flow.entity.ProcessInstanceRecord;
import com.pig4cloud.pigx.flow.entity.ProcessNodeRecordAssignUser;
import com.pig4cloud.pigx.flow.service.IProcessInstanceRecordService;
import com.pig4cloud.pigx.flow.service.IProcessNodeRecordAssignUserService;
import com.pig4cloud.pigx.flow.service.IRemoteService;
import com.pig4cloud.pigx.flow.vo.NodeVo;
import com.pig4cloud.pigx.flow.vo.UserVo;
import lombok.SneakyThrows;
import java.util.*;
import java.util.stream.Collectors;
/**
* 节点格式化显示工具类
* <p>
* 提供流程节点的可视化格式化功能,将流程定义节点转换为前端展示所需的数据结构。 该工具类负责处理节点状态、执行人信息、分支结构等显示逻辑。
* <p>
* 主要功能: 1. 格式化流程节点为可视化数据结构 2. 处理节点的执行状态(未开始、进行中、已完成) 3. 获取并展示节点的执行人信息 4. 处理条件分支和并行分支的嵌套结构
* 5. 支持发起人自选、指定用户、表单人员等多种执行人类型
* <p>
* 使用场景: - 流程图可视化展示 - 流程进度跟踪 - 流程历史查看
*
* @author pigx
*/
public class NodeFormatUtil {
/**
* 格式化流程节点为可视化展示结构
* <p>
* 递归处理流程节点树,生成包含节点状态、执行人信息的展示数据。 支持处理各种节点类型,包括审批节点、抄送节点、网关节点等。
* @param node 当前处理的节点对象
* @param completeNodeSet 已完成的节点ID集合
* @param continueNodeSet 进行中的节点ID集合
* @param processInstanceId 流程实例ID,用于查询历史数据,可为null
* @param paramMap 流程变量Map,包含表单数据和执行人选择信息
* @return 格式化后的节点展示对象列表
*/
@SneakyThrows
public static List<NodeVo> formatProcessNodeShow(Node node, Set<String> completeNodeSet,
Set<String> continueNodeSet, String processInstanceId, Map<String, Object> paramMap) {
List<NodeVo> list = new ArrayList<>();
if (!NodeUtil.isNode(node)) {
return list;
}
String name = node.getName();
Integer type = node.getType();
// SELF_SELECT
NodeVo nodeVo = new NodeVo();
nodeVo.setId(node.getId());
nodeVo.setName(name);
nodeVo.setType(type);
nodeVo.setStatus(NodeStatusEnum.WKS.getCode());
if (completeNodeSet.contains(node.getId())) {
nodeVo.setStatus(NodeStatusEnum.YJS.getCode());
}
if (continueNodeSet.contains(node.getId())) {
nodeVo.setStatus(NodeStatusEnum.JXZ.getCode());
}
{
nodeVo.setPlaceholder(node.getPlaceHolder());
}
List<UserVo> userVoList = new ArrayList<>();
if (type == NodeTypeEnum.APPROVAL.getValue().intValue()) {
Integer assignedType = node.getAssignedType();
boolean selfSelect = assignedType == ProcessInstanceConstant.AssignedTypeClass.SELF_SELECT;
nodeVo.setSelectUser(selfSelect);
if (selfSelect) {
nodeVo.setMultiple(node.getMultiple());
}
// 用户列表
if (StrUtil.isNotBlank(processInstanceId)) {
IProcessNodeRecordAssignUserService processNodeRecordAssignUserService = SpringUtil
.getBean(IProcessNodeRecordAssignUserService.class);
List<ProcessNodeRecordAssignUser> processNodeRecordAssignUserList = processNodeRecordAssignUserService
.lambdaQuery()
.eq(ProcessNodeRecordAssignUser::getNodeId, node.getId())
.eq(ProcessNodeRecordAssignUser::getProcessInstanceId, processInstanceId)
.orderByAsc(ProcessNodeRecordAssignUser::getCreateTime)
.list();
Map<String, List<ProcessNodeRecordAssignUser>> map = processNodeRecordAssignUserList.stream()
.collect(Collectors.groupingBy(ProcessNodeRecordAssignUser::getTaskId));
for (Map.Entry<String, List<ProcessNodeRecordAssignUser>> entry : map.entrySet()) {
List<ProcessNodeRecordAssignUser> value = entry.getValue();
List<UserVo> collect = value.stream().map(w -> {
UserVo userVo = buildUser(w.getUserId());
if (userVo == null) {
return null;
}
userVo.setShowTime(w.getEndTime());
userVo.setApproveDesc(w.getApproveDesc());
userVo.setStatus(w.getStatus());
userVo.setOperType(w.getTaskType());
return userVo;
}).filter(Objects::nonNull).toList();
userVoList.addAll(collect);
}
if (processNodeRecordAssignUserList.isEmpty()) {
if (assignedType == ProcessInstanceConstant.AssignedTypeClass.SELF) {
// 发起人自己
userVoList.addAll(CollUtil.newArrayList(buildRootUser(processInstanceId)));
}
if (assignedType == ProcessInstanceConstant.AssignedTypeClass.SELF_SELECT) {
// 发起人自选
Object variable = paramMap.get(StrUtil.format("{}_assignee_select", node.getId()));
ObjectMapper objectMapper = SpringUtil.getBean(ObjectMapper.class);
List<NodeUser> nodeUserDtos = objectMapper.readValue(objectMapper.writeValueAsString(variable),
new TypeReference<>() {
});
if (CollUtil.isNotEmpty(nodeUserDtos)) {
List<Long> collect = nodeUserDtos.stream().map(NodeUser::getId).toList();
for (Long aLong : collect) {
UserVo userVo = buildUser(aLong);
userVoList.addAll(CollUtil.newArrayList(userVo));
}
}
}
}
}
else if (assignedType == ProcessInstanceConstant.AssignedTypeClass.USER) {
// 指定用户
List<NodeUser> nodeUserList = node.getNodeUserList();
List<UserVo> tempList = buildUser(nodeUserList);
userVoList.addAll(tempList);
}
else if (assignedType == ProcessInstanceConstant.AssignedTypeClass.FORM_USER) {
// 表单人员
String formUser = node.getFormUserId();
Object o = paramMap.get(formUser);
if (o != null) {
ObjectMapper objectMapper = SpringUtil.getBean(ObjectMapper.class);
String jsonString = objectMapper.writeValueAsString(o);
if (StrUtil.isNotBlank(jsonString)) {
List<NodeUser> nodeUserDtoList = objectMapper.readValue(jsonString, new TypeReference<>() {
});
List<Long> userIdList = nodeUserDtoList.stream().map(NodeUser::getId).toList();
for (Long aLong : userIdList) {
userVoList.addAll(CollUtil.newArrayList(buildUser(aLong)));
}
}
}
}
else if (assignedType == ProcessInstanceConstant.AssignedTypeClass.SELF) {
// 发起人自己
userVoList.addAll(CollUtil.newArrayList(buildUser(SecurityUtils.getUser().getId())));
}
else if (assignedType == ProcessInstanceConstant.AssignedTypeClass.EXPRESSION) {
// 表达式
// Object result = getExpressionValue(paramMap, node.getAssignExpress());
// List<UserVo> userVos = Convert.toList(Long.class, result)
// .stream()
// .map(NodeFormatUtil::buildUser)
// .toList();
// userVoList.addAll(userVos);
}
}
else if (Objects.equals(node.getType(), NodeTypeEnum.ROOT.getValue())) {
// 发起节点
if (StrUtil.isBlank(processInstanceId)) {
UserVo userVo = buildUser(SecurityUtils.getUser().getId());
userVoList.addAll(CollUtil.newArrayList(userVo));
}
else {
IProcessInstanceRecordService processInstanceRecordService = SpringUtil
.getBean(IProcessInstanceRecordService.class);
ProcessInstanceRecord processInstanceRecord = processInstanceRecordService.lambdaQuery()
.eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId)
.one();
UserVo userVo = buildRootUser(processInstanceId);
userVo.setShowTime(processInstanceRecord.getCreateTime());
userVo.setStatus(NodeStatusEnum.YJS.getCode());
userVoList.addAll(CollUtil.newArrayList(userVo));
}
}
else if (Objects.equals(node.getType(), NodeTypeEnum.CC.getValue())) {
// 抄送节点
List<NodeUser> nodeUserList = node.getNodeUserList();
List<UserVo> tempList = buildUser(nodeUserList);
userVoList.addAll(tempList);
}
// 根据userVo 的ID 去重userVoList
nodeVo.setUserVoList(CollUtil.distinct(userVoList, UserVo::getId, true));
List<NodeVo> branchList = new ArrayList<>();
if (type == NodeTypeEnum.EXCLUSIVE_GATEWAY.getValue().intValue()
|| type == NodeTypeEnum.PARALLEL_GATEWAY.getValue().intValue()) {
// 条件分支
List<Node> branchs = node.getConditionNodes();
for (Node branch : branchs) {
Node children = branch.getChildren();
List<NodeVo> processNodeShowDtos = formatProcessNodeShow(children, completeNodeSet, continueNodeSet,
processInstanceId, paramMap);
NodeVo p = new NodeVo();
p.setChildren(processNodeShowDtos);
p.setPlaceholder(branch.getPlaceHolder());
branchList.add(p);
}
}
nodeVo.setBranch(branchList);
list.add(nodeVo);
List<NodeVo> next = formatProcessNodeShow(node.getChildren(), completeNodeSet, continueNodeSet,
processInstanceId, paramMap);
list.addAll(next);
return list;
}
/**
* 根据流程实例ID构建发起人用户信息
* <p>
* 查询流程实例记录,获取发起人的用户ID,并构建用户展示对象。 主要用于展示流程的发起人信息。
* @param processInstanceId 流程实例ID
* @return 发起人的用户展示对象
*/
private static UserVo buildRootUser(String processInstanceId) {
IProcessInstanceRecordService processInstanceRecordService = SpringUtil
.getBean(IProcessInstanceRecordService.class);
ProcessInstanceRecord processInstanceRecord = processInstanceRecordService.lambdaQuery()
.eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId)
.one();
Long userId = processInstanceRecord.getUserId();
return buildUser(userId);
}
/**
* 根据用户ID构建用户展示信息
* <p>
* 通过远程服务获取用户详细信息,并构建用户展示对象。 如果用户不存在或为空执行人ID,返回特殊处理的对象。
* @param userId 用户ID
* @return 用户展示对象,如果用户不存在则返回null
*/
private static UserVo buildUser(long userId) {
if (ProcessInstanceConstant.DEFAULT_EMPTY_ASSIGN == userId) {
return UserVo.builder().id(userId).build();
}
RemoteUserService userService = SpringUtil.getBean(RemoteUserService.class);
SysUser user = RetOps.of(userService.getUserById(CollUtil.newArrayList(userId)))
.getData()
.map(sysUsers -> sysUsers.isEmpty() ? null : sysUsers.get(0))
.orElse(null);
if (user == null) {
return null;
}
return UserVo.builder().id(userId).name(user.getName()).avatar(user.getAvatar()).build();
}
/**
* 批量构建用户展示信息
* <p>
* 根据节点用户配置列表,批量获取用户信息并构建展示对象。 支持处理用户类型和部门类型: - 用户类型:直接获取用户信息 - 部门类型:获取部门下的所有用户
* @param nodeUserList 节点用户配置列表,包含用户ID和部门ID
* @return 用户展示对象列表
*/
private static List<UserVo> buildUser(List<NodeUser> nodeUserList) {
List<UserVo> userVoList = new ArrayList<>();
// 用户id
List<Long> userIdList = nodeUserList.stream()
.filter(w -> StrUtil.equals(w.getType(), NodeUserTypeEnum.USER.getKey()))
.map(w -> Convert.toLong(w.getId()))
.toList();
// 部门id
List<Long> deptIdList = nodeUserList.stream()
.filter(w -> StrUtil.equals(w.getType(), NodeUserTypeEnum.DEPT.getKey()))
.map(w -> Convert.toLong(w.getId()))
.toList();
if (CollUtil.isNotEmpty(deptIdList)) {
IRemoteService iRemoteService = SpringUtil.getBean(IRemoteService.class);
List<Long> data = iRemoteService.queryUserIdListByDepIdList(deptIdList).getData();
if (CollUtil.isNotEmpty(data)) {
for (long datum : data) {
if (!userIdList.contains(datum)) {
userIdList.add(datum);
}
}
}
}
{
for (Long aLong : userIdList) {
userVoList.addAll(CollUtil.newArrayList(buildUser(aLong)));
}
}
return userVoList;
}
}
@@ -1,14 +1,14 @@
package com.pig4cloud.pigx.flow.engine.node;
package com.pig4cloud.pigx.flow.support.node;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.pig4cloud.pigx.flow.task.api.feign.RemoteFlowTaskService;
import com.pig4cloud.pigx.flow.task.constant.ProcessInstanceConstant;
import com.pig4cloud.pigx.flow.task.dto.Node;
import com.pig4cloud.pigx.flow.task.dto.NodeUser;
import com.pig4cloud.pigx.flow.constant.ProcessInstanceConstant;
import com.pig4cloud.pigx.flow.dto.Node;
import com.pig4cloud.pigx.flow.dto.NodeUser;
import com.pig4cloud.pigx.flow.service.IProcessNodeDataService;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
@@ -21,21 +21,36 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 多实例任务处理器
* <p>
* 处理Flowable多实例任务(会签、或签、顺序签等)的核心组件。 主要功能包括: 1. 解析多实例任务的执行人列表 2. 判断多实例任务的完成条件
* <p>
* 支持的多实例模式: - 会签:所有人都必须处理,任何人拒绝则任务结束 - 或签:任何一人处理即可完成 - 顺序签:按顺序逐个处理
* <p>
* 该类在流程定义中通过${multiInstanceHandler.resolveAssignee(execution)}方式调用
*
* @author pigx
*/
@Slf4j
@RequiredArgsConstructor
@Component("multiInstanceHandler")
public class MultiInstanceHandler {
private final RemoteFlowTaskService remoteFlowTaskService;
private final IProcessNodeDataService processNodeDataService;
private final Map<String, AssignUserStrategy> assignUserStrategyMap;
private final ObjectMapper objectMapper;
/**
* 解析执行人
* @param execution 流程执行对象
* @return 执行人集合
* 解析多实例任务的执行人列表
* <p>
* 根据节点配置的用户分配策略,动态计算该多实例任务应该分配给哪些用户。 如果节点没有配置或找不到执行人,会尝试从流程变量中获取默认值。
* 如果最终没有找到任何执行人,会返回一个特殊的空标记。
* @param execution 流程执行对象,包含流程实例信息和变量
* @return 执行人ID列表,如果没有执行人则返回包含空标记的列表
* @throws Exception 当JSON解析失败时抛出异常
*/
@SneakyThrows
public List<Long> resolveAssignee(DelegateExecution execution) {
@@ -56,9 +71,10 @@ public class MultiInstanceHandler {
}).get(0);
// 节点数据
Node node = remoteFlowTaskService.queryNodeOriData(flowId, nodeId).getData();
Node node = processNodeDataService.getNodeData(flowId, nodeId).getData();
if (node != null) {
Map<String, Object> variables = execution.getVariables();
variables.put("execution", execution);
Integer assignedType = node.getAssignedType();
List<Long> userIdList = assignUserStrategyMap.get(assignedType + "AssignUserStrategy")
.handle(node, rootUser, variables);
@@ -86,16 +102,21 @@ public class MultiInstanceHandler {
}
/**
* 会签或者或签完成条件检查 检查节点是否满足会签或者或签的完成条件
* @param execution 执行实例对象
* @return boolean 如果节点满足条件则返回true,否则返回false
* 多实例任务完成条件检查
* <p>
* 检查多实例任务是否满足完成条件,支持以下模式: 1. 会签(全部同意):所有人都必须同意,任何人拒绝则立即结束 2. 或签(一人同意):任何一人同意即可完成 3.
* 顺序签:按顺序处理,任何人拒绝则立即结束
* <p>
* 该方法在流程定义中作为completionCondition表达式被调用
* @param execution 流程执行对象,包含多实例的执行信息
* @return true表示满足完成条件,多实例任务结束;false表示继续执行
*/
public boolean completionCondition(DelegateExecution execution) {
ExecutionEntityImpl entity = (ExecutionEntityImpl) execution;
String processDefinitionKey = entity.getProcessDefinitionKey();
String nodeId = execution.getCurrentActivityId();
Node node = remoteFlowTaskService.queryNodeOriData(processDefinitionKey, nodeId).getData();
Node node = processNodeDataService.getNodeData(processDefinitionKey, nodeId).getData();
Integer multipleMode = node.getMultipleMode();
BigDecimal modePercentage = BigDecimal.valueOf(100);
@@ -0,0 +1,90 @@
package com.pig4cloud.pigx.flow.support.node.impl;
import cn.hutool.core.convert.Convert;
import cn.hutool.extra.spring.SpringUtil;
import com.pig4cloud.pigx.flow.constant.ProcessInstanceConstant;
import com.pig4cloud.pigx.flow.dto.Node;
import com.pig4cloud.pigx.flow.dto.NodeUser;
import com.pig4cloud.pigx.flow.support.node.AssignUserStrategy;
import org.flowable.common.engine.api.delegate.Expression;
import org.flowable.common.engine.api.variable.VariableContainer;
import org.flowable.common.engine.impl.el.ExpressionManager;
import org.flowable.common.engine.impl.variable.MapDelegateVariableContainer;
import org.flowable.engine.ManagementService;
import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl;
import org.flowable.engine.impl.util.CommandContextUtil;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
/**
* 表达式分配用户策略实现类:将任务分配给流程发起人本人
*
* @author lengleng
* @date 2025/08/07
*/
@Component(ProcessInstanceConstant.AssignedTypeClass.EXPRESSION + "AssignUserStrategy")
public class AssignExpressStrategyImpl implements AssignUserStrategy {
/**
* 处理发起人分配逻辑
* <p>
* 直接返回流程发起人的用户ID,任务将分配给发起人本人。
* @param node 节点配置对象(本策略中未使用)
* @param rootUser 流程发起人信息,包含发起人的用户ID
* @param variables 流程变量(本策略中未使用)
* @return 包含发起人用户ID的列表
*/
@Override
public List<Long> handle(Node node, NodeUser rootUser, Map<String, Object> variables) {
String assignExpress = node.getAssignExpress();
Object result = getExpressionValue(variables, assignExpress);
return Convert.toList(Long.class, result);
}
/**
* 获取表达式值
* @param variableContainer 变量容器
* @param expressionString 表达式字符串
* @param processEngineConfiguration 流程引擎配置
* @return 表达式计算结果
*/
private static Object getExpressionValue(VariableContainer variableContainer, String expressionString,
ProcessEngineConfigurationImpl processEngineConfiguration) {
assert processEngineConfiguration != null;
ExpressionManager expressionManager = processEngineConfiguration.getExpressionManager();
assert expressionManager != null;
Expression expression = expressionManager.createExpression(expressionString);
return expression.getValue(variableContainer);
}
/**
* 获取表达式值
* @param variableContainer 变量容器
* @param expressionString 表达式字符串
* @return 表达式计算结果
*/
public static Object getExpressionValue(VariableContainer variableContainer, String expressionString) {
ProcessEngineConfigurationImpl processEngineConfiguration = CommandContextUtil.getProcessEngineConfiguration();
if (processEngineConfiguration != null) {
return getExpressionValue(variableContainer, expressionString, processEngineConfiguration);
}
ManagementService managementService = SpringUtil.getBean(ManagementService.class);
assert managementService != null;
return managementService.executeCommand(context -> getExpressionValue(variableContainer, expressionString,
CommandContextUtil.getProcessEngineConfiguration()));
}
/**
* 根据变量映射和表达式字符串获取表达式值
* @param variable 变量映射
* @param expressionString 表达式字符串
* @return 表达式计算结果
*/
public static Object getExpressionValue(Map<String, Object> variable, String expressionString) {
VariableContainer variableContainer = new MapDelegateVariableContainer(variable, VariableContainer.empty());
return getExpressionValue(variableContainer, expressionString);
}
}