diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..3d04518f2 --- /dev/null +++ b/CLAUDE.md @@ -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 \ No newline at end of file diff --git a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/constant/ProcessInstanceConstant.java b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/constant/ProcessInstanceConstant.java new file mode 100644 index 000000000..c64c3f40c --- /dev/null +++ b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/constant/ProcessInstanceConstant.java @@ -0,0 +1,130 @@ +package com.pig4cloud.pigx.flow.constant; + +/** + * 流程实例常量定义 + *

+ * 定义流程实例执行过程中使用的各种常量,包括: - 无执行人处理策略 - 拒绝处理策略 - 多人审批模式 - 任务分配类型 - 表单权限 + *

+ * + * @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"; + + } + +} diff --git a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/Node.java b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/Node.java new file mode 100644 index 000000000..1e9eb72d9 --- /dev/null +++ b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/Node.java @@ -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; + +/** + * 流程节点数据传输对象 + *

+ * 用于流程定义的节点配置信息传输。该对象包含了节点的所有配置信息, 包括节点类型、审批人配置、表单权限、条件配置等。 + * 支持嵌套结构,可以表示复杂的流程结构(如条件分支、并行分支)。 + *

+ * + * @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 nodeUserList; + + /** + * 条件节点列表 当节点类型为条件分支时,存储所有分支节点 + */ + private List conditionNodes; + + /** + * 表单字段权限配置 key=表单字段ID,value=权限类型(H=隐藏,R=只读,E=编辑) + */ + private Map formPerms; + + /** + * 无执行人处理配置 定义当节点找不到执行人时的处理策略 + */ + private Nobody nobody; + + /** + * 分组模式标识 用于条件配置的分组模式 + */ + private Boolean groupMode; + + /** + * 条件列表 当节点为条件分支时,定义各分支的执行条件 + */ + private List conditionList; + + /** + * 拒绝配置 定义节点拒绝后的处理策略 + */ + private Refuse refuse; + +} diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/NodeFormatUtil.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/NodeFormatUtil.java new file mode 100644 index 000000000..643063c9a --- /dev/null +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/NodeFormatUtil.java @@ -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; + +/** + * 节点格式化显示工具类 + *

+ * 提供流程节点的可视化格式化功能,将流程定义节点转换为前端展示所需的数据结构。 该工具类负责处理节点状态、执行人信息、分支结构等显示逻辑。 + *

+ * 主要功能: 1. 格式化流程节点为可视化数据结构 2. 处理节点的执行状态(未开始、进行中、已完成) 3. 获取并展示节点的执行人信息 4. 处理条件分支和并行分支的嵌套结构 + * 5. 支持发起人自选、指定用户、表单人员等多种执行人类型 + *

+ * 使用场景: - 流程图可视化展示 - 流程进度跟踪 - 流程历史查看 + * + * @author pigx + */ +public class NodeFormatUtil { + + /** + * 格式化流程节点为可视化展示结构 + *

+ * 递归处理流程节点树,生成包含节点状态、执行人信息的展示数据。 支持处理各种节点类型,包括审批节点、抄送节点、网关节点等。 + * @param node 当前处理的节点对象 + * @param completeNodeSet 已完成的节点ID集合 + * @param continueNodeSet 进行中的节点ID集合 + * @param processInstanceId 流程实例ID,用于查询历史数据,可为null + * @param paramMap 流程变量Map,包含表单数据和执行人选择信息 + * @return 格式化后的节点展示对象列表 + */ + @SneakyThrows + public static List formatProcessNodeShow(Node node, Set completeNodeSet, + Set continueNodeSet, String processInstanceId, Map paramMap) { + List 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 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 processNodeRecordAssignUserList = processNodeRecordAssignUserService + .lambdaQuery() + .eq(ProcessNodeRecordAssignUser::getNodeId, node.getId()) + .eq(ProcessNodeRecordAssignUser::getProcessInstanceId, processInstanceId) + .orderByAsc(ProcessNodeRecordAssignUser::getCreateTime) + .list(); + Map> map = processNodeRecordAssignUserList.stream() + .collect(Collectors.groupingBy(ProcessNodeRecordAssignUser::getTaskId)); + + for (Map.Entry> entry : map.entrySet()) { + List value = entry.getValue(); + List 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 nodeUserDtos = objectMapper.readValue(objectMapper.writeValueAsString(variable), + new TypeReference<>() { + }); + + if (CollUtil.isNotEmpty(nodeUserDtos)) { + List 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 nodeUserList = node.getNodeUserList(); + List 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 nodeUserDtoList = objectMapper.readValue(jsonString, new TypeReference<>() { + }); + List 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 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 nodeUserList = node.getNodeUserList(); + + List tempList = buildUser(nodeUserList); + userVoList.addAll(tempList); + + } + + // 根据userVo 的ID 去重userVoList + nodeVo.setUserVoList(CollUtil.distinct(userVoList, UserVo::getId, true)); + + List branchList = new ArrayList<>(); + + if (type == NodeTypeEnum.EXCLUSIVE_GATEWAY.getValue().intValue() + || type == NodeTypeEnum.PARALLEL_GATEWAY.getValue().intValue()) { + // 条件分支 + List branchs = node.getConditionNodes(); + + for (Node branch : branchs) { + Node children = branch.getChildren(); + List 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 next = formatProcessNodeShow(node.getChildren(), completeNodeSet, continueNodeSet, + processInstanceId, paramMap); + list.addAll(next); + + return list; + } + + /** + * 根据流程实例ID构建发起人用户信息 + *

+ * 查询流程实例记录,获取发起人的用户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构建用户展示信息 + *

+ * 通过远程服务获取用户详细信息,并构建用户展示对象。 如果用户不存在或为空执行人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(); + } + + /** + * 批量构建用户展示信息 + *

+ * 根据节点用户配置列表,批量获取用户信息并构建展示对象。 支持处理用户类型和部门类型: - 用户类型:直接获取用户信息 - 部门类型:获取部门下的所有用户 + * @param nodeUserList 节点用户配置列表,包含用户ID和部门ID + * @return 用户展示对象列表 + */ + private static List buildUser(List nodeUserList) { + List userVoList = new ArrayList<>(); + // 用户id + List userIdList = nodeUserList.stream() + .filter(w -> StrUtil.equals(w.getType(), NodeUserTypeEnum.USER.getKey())) + .map(w -> Convert.toLong(w.getId())) + .toList(); + // 部门id + List 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 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; + } + +} diff --git a/pigx-flow/pigx-flow-engine/pigx-flow-engine-biz/src/main/java/com/pig4cloud/pigx/flow/engine/node/MultiInstanceHandler.java b/pigx-flow/pigx-flow-engine/pigx-flow-engine-biz/src/main/java/com/pig4cloud/pigx/flow/engine/node/MultiInstanceHandler.java index 7b864c614..047fc4b62 100644 --- a/pigx-flow/pigx-flow-engine/pigx-flow-engine-biz/src/main/java/com/pig4cloud/pigx/flow/engine/node/MultiInstanceHandler.java +++ b/pigx-flow/pigx-flow-engine/pigx-flow-engine-biz/src/main/java/com/pig4cloud/pigx/flow/engine/node/MultiInstanceHandler.java @@ -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; +/** + * 多实例任务处理器 + *

+ * 处理Flowable多实例任务(会签、或签、顺序签等)的核心组件。 主要功能包括: 1. 解析多实例任务的执行人列表 2. 判断多实例任务的完成条件 + *

+ * 支持的多实例模式: - 会签:所有人都必须处理,任何人拒绝则任务结束 - 或签:任何一人处理即可完成 - 顺序签:按顺序逐个处理 + *

+ * 该类在流程定义中通过${multiInstanceHandler.resolveAssignee(execution)}方式调用 + * + * @author pigx + */ @Slf4j @RequiredArgsConstructor @Component("multiInstanceHandler") public class MultiInstanceHandler { - private final RemoteFlowTaskService remoteFlowTaskService; + private final IProcessNodeDataService processNodeDataService; private final Map assignUserStrategyMap; private final ObjectMapper objectMapper; /** - * 解析执行人 - * @param execution 流程执行对象 - * @return 执行人集合 + * 解析多实例任务的执行人列表 + *

+ * 根据节点配置的用户分配策略,动态计算该多实例任务应该分配给哪些用户。 如果节点没有配置或找不到执行人,会尝试从流程变量中获取默认值。 + * 如果最终没有找到任何执行人,会返回一个特殊的空标记。 + * @param execution 流程执行对象,包含流程实例信息和变量 + * @return 执行人ID列表,如果没有执行人则返回包含空标记的列表 + * @throws Exception 当JSON解析失败时抛出异常 */ @SneakyThrows public List 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 variables = execution.getVariables(); + variables.put("execution", execution); Integer assignedType = node.getAssignedType(); List userIdList = assignUserStrategyMap.get(assignedType + "AssignUserStrategy") .handle(node, rootUser, variables); @@ -86,16 +102,21 @@ public class MultiInstanceHandler { } /** - * 会签或者或签完成条件检查 检查节点是否满足会签或者或签的完成条件 - * @param execution 执行实例对象 - * @return boolean 如果节点满足条件则返回true,否则返回false + * 多实例任务完成条件检查 + *

+ * 检查多实例任务是否满足完成条件,支持以下模式: 1. 会签(全部同意):所有人都必须同意,任何人拒绝则立即结束 2. 或签(一人同意):任何一人同意即可完成 3. + * 顺序签:按顺序处理,任何人拒绝则立即结束 + *

+ * 该方法在流程定义中作为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); diff --git a/pigx-flow/pigx-flow-engine/pigx-flow-engine-biz/src/main/java/com/pig4cloud/pigx/flow/engine/node/impl/AssignExpressStrategyImpl.java b/pigx-flow/pigx-flow-engine/pigx-flow-engine-biz/src/main/java/com/pig4cloud/pigx/flow/engine/node/impl/AssignExpressStrategyImpl.java new file mode 100644 index 000000000..d94002bdc --- /dev/null +++ b/pigx-flow/pigx-flow-engine/pigx-flow-engine-biz/src/main/java/com/pig4cloud/pigx/flow/engine/node/impl/AssignExpressStrategyImpl.java @@ -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 { + + /** + * 处理发起人分配逻辑 + *

+ * 直接返回流程发起人的用户ID,任务将分配给发起人本人。 + * @param node 节点配置对象(本策略中未使用) + * @param rootUser 流程发起人信息,包含发起人的用户ID + * @param variables 流程变量(本策略中未使用) + * @return 包含发起人用户ID的列表 + */ + @Override + public List handle(Node node, NodeUser rootUser, Map 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 variable, String expressionString) { + VariableContainer variableContainer = new MapDelegateVariableContainer(variable, VariableContainer.empty()); + return getExpressionValue(variableContainer, expressionString); + } + +}