diff --git a/docs/流程引擎架构分析.md b/docs/流程引擎架构分析.md new file mode 100644 index 0000000..bf38c08 --- /dev/null +++ b/docs/流程引擎架构分析.md @@ -0,0 +1,423 @@ +# 流程引擎架构分析 + +基于 Flowable 6.8.0,Jeecg 在之上封装了自定义的流程元数据管理层,Flovable 仅作为 BPMN 执行内核使用。 + +--- + +## 整体分层 + +``` +┌──────────────────────────────────────────────────┐ +│ 前端 (Vue 3) │ +│ 流程设计器 │ 待办任务 │ 审批表单 │ 流程列表 │ +└──────────────────┬───────────────────────────────┘ + │ REST API +┌──────────────────┴───────────────────────────────┐ +│ ActTaskController │ +│ /act/task/list /act/task/processComplete ... │ +│ /act/process/deploy /act/process/list ... │ +└──────────────────┬───────────────────────────────┘ + │ +┌──────────────────┴───────────────────────────────┐ +│ 业务门面层 (Facade) │ +│ BpmBaseExtApiImpl — 对外统一入口 │ +│ ExtActProcessServiceImpl — 核心编排 (1493行) │ +└──────────────────┬───────────────────────────────┘ + │ + ┌────────────┼────────────┐ + ▼ ▼ ▼ +┌──────────┐ ┌──────────┐ ┌──────────┐ +│ ext_act_ │ │ Flowable │ │ 业务表单 │ +│ 元数据层 │ │ 执行引擎 │ │ 数据层 │ +└──────────┘ └──────────┘ └──────────┘ +``` + +--- + +## 一、自定义元数据层(ext_act_* 14 张表) + +### 1.1 流程定义 — ext_act_process + +**Entity:** `ExtActProcess.java` + +双格式存储 BPMN 定义,同时支持可视化设计器和"简流"低代码 JSON 格式: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | String | 主键 | +| `processKey` | String | 流程标识,跨版本唯一 | +| `processName` | String | 流程名称 | +| `processXml` | byte[] | 标准 BPMN 2.0 XML | +| `processJson` | String | "简流"低代码设计器 JSON | +| `startType` | Integer | 触发方式:手动/表事件/定时器/信号/按钮 | +| `processStatus` | Integer | 0=未部署, 1=已部署 | +| `deploymentId` | String | 关联 Flowable `act_re_deployment.ID_` | +| `processDeployTime` | Date | 最近一次部署时间 | +| `categoryId` | String | 流程分类 | +| `updateCount` | Integer | @Version 乐观锁 | + +**版本管理:** `ext_act_process` 不维护版本号。版本由 Flowable 原生管理——每次 `deployProcess()` 在 `act_re_procdef` 中自动递增 `VERSION_`。历史版本仅在 Flowable 引擎表中保留。 + +### 1.2 表单绑定 — ext_act_process_form + +**Entity:** `ExtActProcessForm.java` + +流程与业务表单的映射关系,运行时启动流程时以此表为依据: + +| 字段 | 说明 | +|------|------| +| `relationCode` | 流程编码(如 `bg_xi_speak_001`),作为流程启动的唯一标识 | +| `processId` | 关联 `ext_act_process.id` | +| `formType` | 1=Online在线表单, 2=设计器表单, 3=自定义开发 | +| `formTableName` | 业务数据表名或设计器表名 | +| `flowStatusCol` | 业务表中存储流程状态的字段名 | +| `titleExp` | 标题表达式,如 `{name}的请假申请` | +| `triggerAction` | 自动触发时机:add/update/delete | +| `startSignalName` | 信号启动名称模式 | + +### 1.3 节点配置(三层结构) + +#### ext_act_process_node — 实时配置 + +运行时直接读取此表。每个节点可配置: + +| 配置项 | 字段 | 说明 | +|--------|------|------| +| 表单可编辑 | `formEditStatus` | 审批时表单是否可修改 | +| 抄送 | `ccStatus` | 是否允许抄送 | +| 自选下一节点 | `selnextUserStatus` | 是否允许选择下一审批人 | +| 消息通知 | `msgStatus` | 是否发送通知 | +| 转办 | `transferStatus` | 是否允许转办 | +| 加签 | `addSignStatus` | 是否允许加签 | +| 驳回 | `rejectStatus` | 是否允许驳回 | +| 计数器加签 | `allowCounterSignAddUser` | 会签中是否允许添加会签人 | +| 扩展配置 | `nodeConfigJson` | 邮件/钉钉/系统通知的详细配置 | + +#### ext_act_process_node_deploy — 部署快照 + +`deployProcess()` 时从 `ext_act_process_node` 复制,按 `deploymentId` 隔离。设计上是为了版本锁定,但**当前代码实际运行时读取的是实时 `ext_act_process_node`**,并非快照。代码注释标注了已知问题: + +```java +// ActTaskController.java line 322 +// TODO bug, 这里读取的不是发布后的配置 +``` + +#### ext_act_process_node_auth — 字段权限 + +按节点+表单字段维度控制:可见/禁用/必填。支持 Online 表单(formType=1)和设计器表单(formType=2)。 + +| 字段 | 说明 | +|------|------| +| `ruleType` | 1=显示, 2=禁用 | +| `required` | 是否必填 | +| `desformComKey` | 设计器表单控件 key | + +### 1.4 运行时关联 — ext_act_flow_data + +**Entity:** `ExtActFlowData.java` + +业务记录与 Flowable 流程实例的桥接表,是运行时状态的核心: + +``` +formDataId + processKey + relationCode → processInstId +``` + +生命周期: + +``` +草稿 (bpmStatus="1") + → 创建记录,processInstId = NULL + +运行中 (bpmStatus="2") + → runtimeService.startProcessInstanceByKey() 后写入 processInstId + → 同步回写业务表 flowStatusCol + +完成 (bpmStatus="3") + → ProcessEndListener 正常结束时更新 + +作废 (bpmStatus="4") + → 流程被作废时更新 + +撤回 (bpmStatus="callbackProcess") + → 删除 ext_act_flow_data 记录,业务表状态重置 +``` + +### 1.5 其他辅助表 + +| 表名 | Entity | 用途 | +|------|--------|------| +| `ext_act_bpm_log` | `ExtActBpmLog` | 审批操作日志,每个节点审批时写入一条 | +| `ext_act_bpm_file` | `ExtActBpmFile` | 审批附件,关联 `bpm_log_id` | +| `ext_act_task_cc` | `ExtActTaskCc` | 任务抄送记录 | +| `ext_act_task_notification` | `ExtActTaskNotification` | 催办/通知记录 | +| `ext_act_listener` | `ExtActListener` | 监听器注册表,供设计器选配 | +| `ext_act_expression` | `ExtActExpression` | 表达式库,候选人公式等 | +| `ext_act_design_flow_data` | `ExtActDesignFlowData` | 设计器表单与流程的运行时关联 | + +--- + +## 二、核心流程:从设计到执行 + +### 2.1 设计阶段 + +``` +┌──────────────┐ saveProcess() ┌───────────────────────┐ +│ 流程设计器 │ ─────────────────────→ │ ext_act_process │ +│ (前端Vue) │ │ .processXml (BPMN) │ +└──────────────┘ │ .processJson (简流) │ + └───────────┬───────────┘ + │ BpmnXMLConverter + │ 解析 UserTask 节点 + ▼ + ┌───────────────────────┐ + │ ext_act_process_node │ + │ 自动创建 + 默认配置 │ + └───────────────────────┘ + + deployProcess() +┌──────────────────────────────────────────────────────────────┐ +│ 1. repositoryService.createDeployment(processXml) │ +│ → act_re_deployment + act_re_procdef + act_ge_bytearray │ +│ 2. 快照 ext_act_process_node → ext_act_process_node_deploy │ +│ 3. 更新 processStatus = 1, processDeployTime │ +└──────────────────────────────────────────────────────────────┘ +``` + +### 2.2 启动阶段 + +``` +BpmBaseExtApiImpl.startMutilProcess(flowCode, formDataId, formUrl, username, jsonData) + │ + ├─ 1. 查 ext_act_process_form WHERE relationCode = flowCode + ├─ 2. 查业务数据 (SQL / MongoDB) + ├─ 3. 组装流程变量: + │ BPM_DATA_ID = formDataId + │ BPM_FORM_KEY = flowCode + │ BPM_FORM_CONTENT_URL = formUrl + │ BPM_BIZ_TITLE = 标题表达式解析结果 + │ BPM_STATUS = "2" (处理中) + │ BPM_FORM_TYPE = 1/2/3 + │ json_data = 业务表单数据 JSON + │ + ├─ 4. identityService.setAuthenticatedUserId(initiator) + ├─ 5. runtimeService.startProcessInstanceByKey(processKey, businessKey, variables) + │ → 返回 processInstanceId + │ + ├─ 6. 创建/更新 ext_act_flow_data (businessKey → processInstanceId 映射) + └─ 7. 回写业务表 flowStatusCol = "2" +``` + +### 2.3 审批阶段 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ GET /act/task/list │ +│ → ActivitiServiceImpl.findPriTodoTasks() │ +│ 返回当前用户的待办任务列表 │ +└─────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────┐ +│ GET /act/task/getProcessTaskTransInfo?taskId=xxx │ +│ → 查询 ext_act_process_node(实时配置) │ +│ 返回: │ +│ formEditStatus — 表单是否可编辑 │ +│ ccStatus — 是否允许抄送 │ +│ selnextUserStatus — 是否允许自选下一节点 │ +│ rejectStatus — 是否允许驳回 │ +│ transferStatus — 是否允许转办 │ +│ addSignStatus — 是否允许加签 │ +│ bpmLogList — 审批历史时间线 │ +│ transitionList — 可选的下一步操作 │ +└─────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────┐ +│ POST /act/task/processComplete │ +│ → ExtActTaskCcServiceImpl.processComplete() │ +│ │ +│ 1. 设置 BPM_STATUS = "2" │ +│ 2. 写入审批结果变量 approve_result_{taskDefKey} │ +│ 3. 判断会签 → 设置候选人列表变量 │ +│ 4. 路由分发: │ +│ · model="1" 单分支 → handTaskComplete() │ +│ └ taskService.complete(taskId, variables) │ +│ · model="2" 多分支 → SuperTaskService.goProcessTaskNode()│ +│ └ runtimeService.createChangeActivityStateBuilder() │ +│ .moveActivityIdTo(nextNode) │ +│ · 驳回 → backToStepTask() │ +│ └ .moveActivityIdTo(历史节点) │ +│ 5. 处理加签/委派子任务 │ +│ 6. saveTaskHandBpmLog() → ext_act_bpm_log │ +│ 7. TaskUpdateFormDataListener 刷新流程变量 │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 2.4 结束阶段 + +``` +流程到达结束事件 + → ProcessEndListener.notify(execution) + │ + ├─ BPM_STATUS = "callbackProcess" + │ → delete ext_act_flow_data + │ → 业务表状态重置为 "1" + │ + ├─ BPM_STATUS = "invalidProcess" + │ → ext_act_flow_data.bpmStatus = "4" + │ → 业务表 flowStatusCol = "4" + │ + └─ 其他(正常完成) + → ext_act_flow_data.bpmStatus = "3" + → 业务表 flowStatusCol = "3" +``` + +--- + +## 三、两种流程定义方式 + +| 维度 | DB 定义流程 | Java 定义流程 | +|------|------------|-------------| +| **BPMN 来源** | 可视化设计器 → `ext_act_process.processXml` | 手工编写 BPMN XML | +| **监听器** | XML 中注册全限定类名 | `@Component` Spring Bean | +| **表达式** | 直接写在 XML 条件中 | Java 方法:`${xiSpeakFlow.getNeedSdwApproval()}` | +| **节点配置** | 前端设计器编辑 → `ext_act_process_node` | 同左或手动插入 | +| **示例** | 通用审批流程 | `XiSpeakFlow`、`XiSpeakFeedbackFlow` | +| **部署方式** | `deployProcess()` | 同样 `deployProcess()` | +| **代码位置** | — | `jeecg-module-flow` | + +### XiSpeak 示例 + +`XiSpeakFlow.java` 注册为 Spring Bean `"xiSpeakFlow"`,在 BPMN XML 中通过表达式回调: + +```xml + + ${xiSpeakFlow.getNeedSdwApproval(jsonData)} + +``` + +业务监听器在 BPMN XML 中以全限定类名注册: + +```xml + +``` + +--- + +## 四、特殊流程能力 + +| 能力 | 实现方式 | 关键类 | +|------|---------|--------| +| **会签** | Flowable `multiInstanceLoopCharacteristics`,运行时 XML 解析判断 | `SuperTaskService.checkUserTaskIsHuiQian()` | +| **加签** | Flowable `Command` 动态修改 BPMN 模型,插入新 UserTask | `AfterSignUserTaskCmd` | +| **驳回** | `runtimeService.createChangeActivityStateBuilder().moveActivityIdTo(历史节点)` | `ExtActTaskCcServiceImpl.backToStepTask()` | +| **转办** | `taskService.setAssignee(taskId, newUserId)` | `ActTaskController.complaint()` | +| **委派** | `taskService.delegateTask(taskId, delegateUser)` | `ActTaskController.delegate()` | +| **撤回** | 设 `BPM_STATUS = "callbackProcess"` → `actProcessService.stopProcessInstanceById()` | `ActTaskController.callbackProcess()` | +| **任务变量刷新** | 每个 UserTask 创建时从业务库拉最新表单数据注入流程变量 | `TaskUpdateFormDataListener` | +| **信号启动** | 低代码"简流"模式,通过信号事件触发子流程 | `SignalProcessStartListener` | + +--- + +## 五、数据库表全景 + +### Flowable 引擎表(47张) + +| 类别 | 前缀 | 数量 | 说明 | +|------|------|------|------| +| 运行时 | `act_ru_*` | 13 | 运行中的流程实例、任务、变量 | +| 历史 | `act_hi_*` | 10 | 已完成的流程实例历史 | +| 仓库 | `act_re_*` | 3 | 流程定义、部署、模型 | +| 通用 | `act_ge_*` | 2 | 字节数组、属性 | +| 身份 | `act_id_*` | 8 | 用户、组、权限 | +| 事件 | `act_evt_*` / `flw_*` | 11 | 事件日志、事件定义、批量 | + +### 自定义表(14张) + +| 类别 | 表名 | 用途 | +|------|------|------| +| 流程定义 | `ext_act_process` | 流程定义(BPMN XML + 简流 JSON) | +| 表单绑定 | `ext_act_process_form` | 流程与业务表单映射 | +| 节点配置 | `ext_act_process_node` | 节点实时配置 | +| 节点快照 | `ext_act_process_node_deploy` | 部署时节点配置快照 | +| 字段权限 | `ext_act_process_node_auth` | 节点字段级权限 | +| 运行时关联 | `ext_act_flow_data` | 业务数据 ↔ 流程实例 | +| 设计器运行时 | `ext_act_design_flow_data` | 设计器表单 ↔ 流程实例 | +| 审批日志 | `ext_act_bpm_log` | 审批操作记录 | +| 审批附件 | `ext_act_bpm_file` | 审批附件 | +| 抄送 | `ext_act_task_cc` | 任务抄送 | +| 催办通知 | `ext_act_task_notification` | 催办记录 | +| 监听器 | `ext_act_listener` | 监听器注册表 | +| 表达式 | `ext_act_expression` | 表达式库 | + +### 业务表(与流程绑定) + +| 表名 | 模块 | 说明 | +|------|------|------| +| `task_task` | tasktask | 事项任务计划表,核心督办任务记录 | +| `bg_xi_speak` | xispeak | 习总书记重要讲话指示批示 | +| `bg_xi_speak_feedback` | xispeak | 讲话指示批示反馈 | +| `task_approval_opinion` | taskapprovalopinion | 审批意见快照 | +| `dj_inspect_improve` | inspectimprove | 巡视整改台账 | + +--- + +## 六、关键文件速查 + +``` +jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/ +├── extbpm/process/ +│ ├── entity/ +│ │ ├── ExtActProcess.java ext_act_process 流程定义 +│ │ ├── ExtActProcessForm.java ext_act_process_form 表单绑定 +│ │ ├── ExtActProcessNode.java ext_act_process_node 节点实时配置 +│ │ ├── ExtActProcessNodeDeployment.java ext_act_process_node_deploy 部署快照 +│ │ ├── ExtActProcessNodePermission.java ext_act_process_node_auth 字段权限 +│ │ ├── ExtActFlowData.java ext_act_flow_data 运行时关联 +│ │ ├── ExtActBpmLog.java ext_act_bpm_log 审批日志 +│ │ ├── ExtActBpmFile.java ext_act_bpm_file 审批附件 +│ │ ├── ExtActTaskCc.java ext_act_task_cc 抄送 +│ │ └── ExtActListener.java ext_act_listener 监听器注册 +│ ├── service/impl/ +│ │ ├── ExtActProcessServiceImpl.java ★ 核心编排服务 (1493行) +│ │ ├── ExtActTaskCcServiceImpl.java 审批完成+路由逻辑 +│ │ ├── ExtActBpmLogServiceImpl.java 审批日志双写(旧表+新意见表) +│ │ └── BpmBaseExtApiImpl.java 对外门面 Facade +│ ├── listener/execution/ +│ │ ├── ProcessEndListener.java 流程结束状态同步 +│ │ ├── TaskUpdateFormDataListener.java 任务创建时刷新表单变量 +│ │ └── SignalProcessStartListener.java 简流信号启动 +│ └── common/ +│ └── WorkFlowGlobals.java 流程常量定义 +├── bpm/ +│ ├── controller/ +│ │ ├── ActTaskController.java 任务操作全部REST端点 (1633行) +│ │ └── ActProcessInstanceController.java 流程实例管理端点 +│ ├── service/impl/ +│ │ ├── ActivitiServiceImpl.java 任务查询+多实例+加签 +│ │ ├── ActProcessService.java Flowable原生部署/激活/挂起 +│ │ └── SuperTaskService.java 节点跳跃+会签判断+转办 +│ └── cmd/ +│ └── AfterSignUserTaskCmd.java 动态加签 Command +└── taskapprovalopinion/ + ├── entity/TaskApprovalOpinion.java task_approval_opinion 审批意见快照 + └── service/impl/ CRUD 实现 + +jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/ +├── xispeak/ +│ ├── XiSpeakFlow.java 表达 Bean ($xiSpeakFlow) +│ ├── XiSpeakConfig.java YAML 配置 +│ └── listener/ 7个业务监听器 +└── xispeakfb/ + ├── XiSpeakFeedbackFlow.java 反馈流程表达 Bean + └── listener/ 反馈流程业务监听器 +``` + +--- + +## 七、关键设计决策 + +1. **双层定义**:`ext_act_process` 存 BPMN XML + 简流 JSON,同时服务可视化设计器和低代码平台 +2. **实时 vs 快照**:节点配置分 `ext_act_process_node`(实时)和 `ext_act_process_node_deploy`(快照),当前运行时实际读取实时表 +3. **双写审批记录**:`ExtActBpmLog`(旧表)+ `TaskApprovalOpinion`(新表),旧表写入成功后尽力双写新表,新表失败不影响主流程 +4. **门面模式**:`BpmBaseExtApiImpl` 是外部系统调用流程能力的唯一入口,隔离了内部实现细节 +5. **Java Bean 表达式**:XiSpeak 流程通过 Spring Bean 方法注入业务逻辑到 BPMN 条件表达式中,实现比 UEL 表达式更复杂的动态计算 diff --git a/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/XiSpeakConfig.java b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/XiSpeakConfig.java index c69c064..41023d5 100644 --- a/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/XiSpeakConfig.java +++ b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/XiSpeakConfig.java @@ -42,5 +42,6 @@ public class XiSpeakConfig { private String temImplLeaderKey; private String temBgLeaderKey; private String sdwLeaderListKey; + private String isEndKey; } } diff --git a/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/XiSpeakFlow.java b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/XiSpeakFlow.java index d0f9857..a5ceb7c 100644 --- a/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/XiSpeakFlow.java +++ b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/XiSpeakFlow.java @@ -10,6 +10,9 @@ import org.flowable.engine.RuntimeService; import org.flowable.engine.delegate.DelegateExecution; import org.jeecg.common.constant.SymbolConstant; import org.jeecg.common.system.api.ISysBaseAPI; +import org.jeecg.modules.bg.xispeak.entity.BgXiSpeak; +import org.jeecg.modules.bg.xispeak.entity.DeptApproveDetail; +import org.jeecg.modules.bg.xispeak.service.IBgXiSpeakService; import org.jeecg.modules.extbpm.process.common.expression.FlowNodeExpression; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -26,6 +29,7 @@ public class XiSpeakFlow { private final ISysBaseAPI iSysBaseAPI; private final RuntimeService runtimeService; private final FlowNodeExpression flowNodeExpression; + private final IBgXiSpeakService bgXiSpeakService; //流程表达式内用法 ${xiSpeakFlow.getBgDeptLdUserIdList()} public List getBgDeptLdUserIdList() { @@ -222,6 +226,51 @@ public class XiSpeakFlow { return flowNodeExpression.getSonProcessHqVariableList(execution,xiSpeakConfig.getXiSpeak().getTemBgLeaderKey()).size(); } + /** + * 流程表达式内用法: ${xiSpeakFlow.getImplDeptLeaderIsEnd(execution)=='1'} + * 优先从业务表单 JSON 读取,没有则回退到流程变量(兼容旧流程实例) + */ + public String getImplDeptLeaderIsEnd(DelegateExecution execution) { + DeptApproveDetail detail = getDeptApproveDetail(execution); + if (detail != null && detail.getIsEnd() != null) { + return String.valueOf(detail.getIsEnd()); + } + // 兜底:从流程变量读取(兼容旧流程) + Object value = flowNodeExpression.getSonProcessVariable(execution, xiSpeakConfig.getXiSpeak().getIsEndKey()); + return value == null ? "0" : value.toString(); + } + + /** + * 流程表达式内用法: ${xiSpeakFlow.getImplDeptLeader(execution)} + */ + public String getImplDeptLeader(DelegateExecution execution) { + DeptApproveDetail detail = getDeptApproveDetail(execution); + if (detail != null && detail.getIsEnd() != null) { + return String.valueOf(detail.getImplWorker()); + } + return detail == null ? StringUtils.EMPTY_STRING : detail.getImplWorker(); + } + + /** + * 流程表达式内用法: ${xiSpeakFlow.getImplDeptWorkerListFromJson(execution)} + * 优先从业务表单 JSON 读取,没有则回退到流程变量(兼容旧流程实例) + */ + public List getImplDeptWorkerListFromJson(DelegateExecution execution) { + DeptApproveDetail detail = getDeptApproveDetail(execution); + if (detail != null && detail.getIsEnd() != null) { + return detail.getImplUserNameList(); + } + return Collections.emptyList(); + } + + /** + * 流程表达式内用法: ${xiSpeakFlow.getImplDeptWorkerListLengthFromJson(execution)} + * 优先从业务表单 JSON 读取,没有则回退到流程变量(兼容旧流程实例) + */ + public int getImplDeptWorkerListLengthFromJson(DelegateExecution execution) { + return this.getImplDeptWorkerListFromJson(execution).size(); + } + /** * 流程表达式内用法: ${xiSpeakFlow.getTemBgLeader(execution)} @@ -232,6 +281,35 @@ public class XiSpeakFlow { + /** + * 从执行实例中获取当前部门ID并查询业务表单 JSON 中的部门审批详情。 + */ + private DeptApproveDetail getDeptApproveDetail(DelegateExecution execution) { + if (execution == null) { + return null; + } + String businessKey = java.util.Objects.toString( + execution.getVariable(xiSpeakConfig.getXiSpeak().getBusinessKey()), null); + String implDeptKey = xiSpeakConfig.getXiSpeak().getImplDeptCollectionUsedKey(); + Object rawDeptVar = execution.getVariableLocal(implDeptKey); + if (rawDeptVar == null) { + rawDeptVar = execution.getVariable(implDeptKey); + } + if (org.apache.commons.lang.StringUtils.isBlank(businessKey) || rawDeptVar == null) { + return null; + } + String deptId = rawDeptVar.toString().replace("[", "").replace("]", "").trim(); + BgXiSpeak xiSpeak = bgXiSpeakService.getById(businessKey); + if (xiSpeak == null || xiSpeak.getApproveInfo() == null) { + return null; + } + return xiSpeak.getApproveInfo().get(deptId); + } + // 流程表达式用法${xiSpeakFlow.getImplDeptIsEnd(execution)==1} + public int getImplDeptIsEnd(DelegateExecution execution){ + return getDeptApproveDetail(execution).getIsEnd(); + } + /** * 抽取出的公共逻辑:从当前执行实例获取局部变量并转为字符串 */ @@ -315,42 +393,40 @@ public class XiSpeakFlow { return resultList.size(); } + /** + * 优先从业务表单 JSON 读取 implUserNameList / implWorker,没有则回退到流程变量(兼容旧流程实例) + */ public List getDeptWorkerList(DelegateExecution execution) { + DeptApproveDetail detail = getDeptApproveDetail(execution); + if (detail != null) { + // isEnd=0 时读取 implUserNameList + if (detail.getImplUserNameList() != null && !detail.getImplUserNameList().isEmpty()) { + return detail.getImplUserNameList(); + } + // isEnd=1 时读取 implWorker(兼容旧数据) + if (org.apache.commons.lang.StringUtils.isNotBlank(detail.getImplWorker())) { + return Collections.singletonList(detail.getImplWorker()); + } + } + // 兜底:从流程变量读取(兼容旧流程) String variableName = xiSpeakConfig.getXiSpeak().getDeptWorkerKey(); if (execution == null || org.apache.commons.lang.StringUtils.isBlank(variableName)) { return Collections.emptyList(); } - try { - // 1. 优先尝试从当前 Execution 的局部作用域获取 - // 在多实例(会签)中,每个分支特有的变量(如 item 变量)通常存储在这里 Object value = execution.getVariableLocal(variableName); - - // 2. 如果局部没有,直接使用 getVariable 获取 - // 该方法会自动向上追溯:当前 Execution -> 父 Execution -> 流程实例(Global) if (value == null) { value = execution.getVariable(variableName); } - - // 3. 结果处理 if (value != null) { - return splitUsernames(value); + return Arrays.stream(String.valueOf(value).split(SymbolConstant.COMMA)) + .map(String::trim) + .filter(org.apache.commons.lang.StringUtils::isNotBlank) + .collect(Collectors.toList()); } } catch (Exception e) { - log.warn("getSonProcessVariable 失败, executionId={}, variableName={}", - execution.getId(), variableName, e); + log.warn("getDeptWorkerList from process variable failed", e); } - return Collections.emptyList(); } - private List splitUsernames(Object value) { - if (value == null) { - return Collections.emptyList(); - } - return Arrays.stream(String.valueOf(value).split(SymbolConstant.COMMA)) - .map(String::trim) - .filter(org.apache.commons.lang.StringUtils::isNotBlank) - .collect(Collectors.toList()); - } - } diff --git a/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeakfb/XiSpeakFeedbackFlow.java b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeakfb/XiSpeakFeedbackFlow.java index 75c6673..5d8f15f 100644 --- a/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeakfb/XiSpeakFeedbackFlow.java +++ b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeakfb/XiSpeakFeedbackFlow.java @@ -115,6 +115,56 @@ public class XiSpeakFeedbackFlow { .orElse(""); } + // 流程表达式内用法 ${XiSpeakFeedbackFlow.getImplDeptIsEnd(execution)} + public Integer getImplDeptIsEnd(DelegateExecution execution) { + DeptApproveDetail detail = getDeptApproveDetailByTask(execution); + if (detail == null || detail.getIsEnd() == null) { + return 0; + } + return detail.getIsEnd(); + } + + // 流程表达式内用法 ${XiSpeakFeedbackFlow.getImplLeaderAsWorker(execution)} + public String getImplLeaderAsWorker(DelegateExecution execution) { + DeptApproveDetail detail = getDeptApproveDetailByTask(execution); + if (detail == null || StringUtils.isBlank(detail.getImplWorker())) { + return StringUtils.EMPTY; + } + return detail.getImplWorker(); + } + + /** + * 从 execution 获取 businessKey → TaskTask → BgXiSpeak → DeptApproveDetail + */ + private DeptApproveDetail getDeptApproveDetailByTask(DelegateExecution execution) { + String businessKey = execution.getProcessInstanceBusinessKey(); + + if (StringUtils.isBlank(businessKey)) { + log.warn("流程 BusinessKey 缺失,ExecutionId: {}", execution.getId()); + return null; + } + + TaskTask taskTask = taskTaskService.getById(businessKey); + if (taskTask == null || StringUtils.isBlank(taskTask.getBusinessId())) { + log.warn("无法获取任务数据或业务关联ID![taskTaskId: {}]", businessKey); + return null; + } + + BgXiSpeak xiSpeak = bgXiSpeakService.getById(taskTask.getBusinessId()); + if (xiSpeak == null || xiSpeak.getApproveInfo() == null) { + log.warn("业务表单数据或审批配置缺失![xiBusinessId: {}]", taskTask.getBusinessId()); + return null; + } + + String deptId = taskTask.getDeptId(); + if (StringUtils.isBlank(deptId)) { + log.warn("中间表表单部门信息缺失![tasktaskId: {}]", taskTask.getId()); + return null; + } + + return xiSpeak.getApproveInfo().get(deptId); + } + /** * 获取当前流程的经办部门的经办人。 *

用于流程表达式判断,通过 BusinessKey 关联业务主键,获取当前经办人。

diff --git a/jeecg-boot-module/jeecg-module-flow/src/main/resources/application-flow.yml b/jeecg-boot-module/jeecg-module-flow/src/main/resources/application-flow.yml index 3430518..dc72fdd 100644 --- a/jeecg-boot-module/jeecg-module-flow/src/main/resources/application-flow.yml +++ b/jeecg-boot-module/jeecg-module-flow/src/main/resources/application-flow.yml @@ -17,6 +17,7 @@ flow-biz: tem-impl-leader-key: "tem_impl_leader" tem-bg-leader-key: "tem_bg_leader" sdw-leader-list-key: "sdwLeaderList" + is-end-key: "isEnd" xi-speak-fb: dept-id: "x" business-id: "business_id" diff --git a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/bpm/controller/ActProcessController.java b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/bpm/controller/ActProcessController.java index 5cdcfc2..9febc34 100644 --- a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/bpm/controller/ActProcessController.java +++ b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/bpm/controller/ActProcessController.java @@ -19,6 +19,7 @@ import org.jeecg.common.util.RedisUtil; import org.jeecg.common.util.TokenUtils; import org.jeecg.common.util.oConvertUtils; import org.jeecg.modules.bpm.dto.ProcessVariableDTO; +import org.jeecg.modules.bpm.service.BpmVariableLocalService; import org.jeecg.modules.bpm.service.impl.ActProcessService; import org.jeecg.modules.extbpm.process.service.IExtActProcessService; import org.jeecg.modules.extbpm.util.CommonRandomUtil; @@ -57,6 +58,8 @@ public class ActProcessController { @Autowired private TaskService taskService; @Autowired + private BpmVariableLocalService bpmVariableLocalService; + @Autowired private ActProcessService actProcessService; @Autowired private IExtActProcessService extActProcessService; @@ -115,6 +118,24 @@ public class ActProcessController { return Result.OK("保存成功"); } + @PostMapping(value = "/setBpmVariableLocal") + @Operation(summary = "设置局部流程变量(向上追溯2层父级执行实例)") + public Result setBpmVariableLocal(@RequestBody ProcessVariableDTO dto) { + if (dto == null || oConvertUtils.isEmpty(dto.getTaskId())) { + return Result.error("任务ID不能为空"); + } + if (oConvertUtils.isEmpty(dto.getVarField())) { + return Result.error("流程变量名不能为空"); + } + try { + bpmVariableLocalService.setVariableLocal(dto.getTaskId(), dto.getVarField(), dto.getVarVal()); + return Result.OK("保存成功"); + } catch (Exception e) { + log.error("设置BPM局部变量失败", e); + return Result.error("保存失败: " + e.getMessage()); + } + } + @PostMapping("deploy") @Operation(summary = "部署流程文件") public Result deploy(@RequestParam("processFile") MultipartFile file) throws IOException { diff --git a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/bpm/service/BpmVariableLocalService.java b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/bpm/service/BpmVariableLocalService.java new file mode 100644 index 0000000..bea3c35 --- /dev/null +++ b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/bpm/service/BpmVariableLocalService.java @@ -0,0 +1,13 @@ +package org.jeecg.modules.bpm.service; + +public interface BpmVariableLocalService { + + /** + * 将变量存入向上追溯2层的父级执行实例中(与 AfterImplWorkerTemStoreListener 逻辑一致) + * + * @param taskId 当前任务ID + * @param varField 变量名 + * @param varVal 变量值 + */ + void setVariableLocal(String taskId, String varField, Object varVal); +} diff --git a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/bpm/service/impl/BpmVariableLocalServiceImpl.java b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/bpm/service/impl/BpmVariableLocalServiceImpl.java new file mode 100644 index 0000000..e8c6b7b --- /dev/null +++ b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/bpm/service/impl/BpmVariableLocalServiceImpl.java @@ -0,0 +1,59 @@ +package org.jeecg.modules.bpm.service.impl; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.TaskService; +import org.flowable.engine.runtime.Execution; +import org.flowable.task.api.Task; +import org.jeecg.common.util.oConvertUtils; +import org.jeecg.modules.bpm.service.BpmVariableLocalService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class BpmVariableLocalServiceImpl implements BpmVariableLocalService { + + private final RuntimeService runtimeService; + private final TaskService taskService; + + @Override + public void setVariableLocal(String taskId, String varField, Object varVal) { + Task task = taskService.createTaskQuery().taskId(taskId).singleResult(); + if (task == null) { + throw new IllegalArgumentException("任务ID不存在: " + taskId); + } + + String executionId = task.getExecutionId(); + + Execution currentExecution = runtimeService.createExecutionQuery() + .executionId(executionId) + .singleResult(); + if (currentExecution == null) { + runtimeService.setVariableLocal(executionId, varField, varVal); + return; + } + + String parentId = currentExecution.getParentId(); + if (oConvertUtils.isEmpty(parentId)) { + runtimeService.setVariableLocal(executionId, varField, varVal); + return; + } + + Execution parentExecution = runtimeService.createExecutionQuery() + .executionId(parentId) + .singleResult(); + if (parentExecution == null) { + runtimeService.setVariableLocal(parentId, varField, varVal); + return; + } + + String grandParentId = parentExecution.getParentId(); + String targetId = oConvertUtils.isNotEmpty(grandParentId) ? grandParentId : parentId; + + runtimeService.setVariableLocal(targetId, varField, varVal); + log.info("BPM局部变量已存入: targetId={}, varField={}, varVal={}", targetId, varField, varVal); + } +} diff --git a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/common/expression/FlowNodeExpression.java b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/common/expression/FlowNodeExpression.java index d7344c7..f9a36ad 100644 --- a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/common/expression/FlowNodeExpression.java +++ b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/common/expression/FlowNodeExpression.java @@ -427,6 +427,43 @@ public class FlowNodeExpression { return this.getSonProcessHqVariableList(execution,variableName).size(); } + /** + * 获取子流程原始变量值(不经过 splitUsernames 逗号分割),适用于非列表类型的变量。 + * 遍历逻辑与 getSonProcessHqVariableList 一致。 + */ + public Object getSonProcessHqVariable(DelegateExecution execution, String variableName) { + if (execution == null || StringUtils.isBlank(variableName)) { + return null; + } + try { + String parentExecutionId = execution.getParentId(); + if (StringUtils.isBlank(parentExecutionId)) { + return null; + } + if (execution.hasVariableLocal(variableName)) { + return execution.getVariableLocal(variableName); + } + + List executions = runtimeService.createExecutionQuery() + .processInstanceId(parentExecutionId) + .list(); + for (Execution childExecution : executions) { + if (execution.getId().equals(childExecution.getId())) { + continue; + } + Object localVariable = runtimeService.getVariableLocal(childExecution.getId(), variableName); + if (localVariable != null) { + return localVariable; + } + } + return execution.getVariable(variableName); + } catch (Exception e) { + log.warn("getSonProcessHqVariable failed, executionId={}, variableName={}", + execution.getId(), variableName, e); + return null; + } + } + public String getSonProcessHqVariableString(DelegateExecution execution, String variableName) { if(execution == null || StringUtils.isBlank(variableName) || getSonProcessHqVariableList(execution, variableName).isEmpty()) return StringUtils.EMPTY; return getSonProcessHqVariableList(execution, variableName).get(0); diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/controller/BgXiSpeakController.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/controller/BgXiSpeakController.java index bd686b4..fecc1df 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/controller/BgXiSpeakController.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/controller/BgXiSpeakController.java @@ -266,6 +266,26 @@ public class BgXiSpeakController extends JeecgController saveSubApprove(@RequestBody Map params) { + String mainId = (String) params.get("mainId"); + String deptId = (String) params.get("deptId"); + Integer isEnd = params.get("isEnd") != null ? Integer.valueOf(params.get("isEnd").toString()) : null; + String implWorker = (String) params.get("implWorker"); + @SuppressWarnings("unchecked") + List implUserNameList = (List) params.get("implUserNameList"); + if (oConvertUtils.isEmpty(mainId) || oConvertUtils.isEmpty(deptId)) { + return Result.error("主表ID和部门ID不能为空"); + } + bgXiSpeakService.saveSubApprove(mainId, deptId, isEnd, implWorker, implUserNameList); + return Result.OK("保存成功"); + } + /** * 保存业务数据同时更新流程变量jsonData * @param id diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/entity/DeptApproveDetail.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/entity/DeptApproveDetail.java index 1516e20..e067a2a 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/entity/DeptApproveDetail.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/entity/DeptApproveDetail.java @@ -14,6 +14,8 @@ public class DeptApproveDetail { private String deptId; // 部门ID private String approverId; // 审批人ID(从监听器获取的) private String approverName; // 审批人姓名(可选) + private Integer isEnd; // 是否办结(0=分配部门经办人,1=办结) + private String implWorker; // 被分配的部门经办人用户名 @JsonAlias("implUserId") private List implUserIdList; // 落实人Id @JsonAlias("implUserName") diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/service/IBgXiSpeakService.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/service/IBgXiSpeakService.java index 0ff70f6..ffb6894 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/service/IBgXiSpeakService.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/service/IBgXiSpeakService.java @@ -85,4 +85,10 @@ public interface IBgXiSpeakService extends IService { List getRootListWithDept(String pid); void checkAndMarkCompletedByProcessInstId(String processInstanceId); + + /** + * 保存部门审批信息(isEnd、部门经办人/经办人列表)到业务表单 approveInfo JSON 中 + * isEnd=1 时 implWorker 生效,isEnd=0 时 implUserNameList 生效 + */ + void saveSubApprove(String mainId, String deptId, Integer isEnd, String implWorker, List implUserNameList); } diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/service/impl/BgXiSpeakServiceImpl.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/service/impl/BgXiSpeakServiceImpl.java index 482686a..3156842 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/service/impl/BgXiSpeakServiceImpl.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/service/impl/BgXiSpeakServiceImpl.java @@ -8,6 +8,8 @@ import org.jeecg.common.exception.JeecgBootException; import org.jeecg.common.util.oConvertUtils; import org.jeecg.common.system.vo.SelectTreeModel; import org.jeecg.modules.bg.xispeak.entity.BgXiSpeak; +import org.jeecg.modules.bg.xispeak.entity.DeptApproveDetail; +import org.jeecg.modules.bg.xispeak.entity.DeptApproveDetailMap; import org.jeecg.modules.bg.xispeak.mapper.BgXiSpeakMapper; import org.jeecg.modules.bg.xispeak.service.IBgXiSpeakService; import org.jeecg.modules.taskapprovalopinion.entity.TaskApprovalOpinion; @@ -351,4 +353,40 @@ public class BgXiSpeakServiceImpl extends ServiceImpl implUserNameList) { + BgXiSpeak xiSpeak = this.getById(mainId); + if (xiSpeak == null) { + throw new JeecgBootException("业务表单不存在"); + } + + DeptApproveDetailMap approveMap = xiSpeak.getApproveInfo(); + if (approveMap == null) { + approveMap = new DeptApproveDetailMap(); + } + + DeptApproveDetail detail = approveMap.get(deptId); + if (detail == null) { + detail = new DeptApproveDetail(); + detail.setDeptId(deptId); + detail.setImplUserNameList(new ArrayList<>()); + approveMap.put(deptId, detail); + } + + detail.setIsEnd(isEnd); + if (isEnd != null && isEnd == 1) { + detail.setImplWorker(implWorker); + detail.setImplUserNameList(null); + } else { + detail.setImplWorker(null); + detail.setImplUserNameList(implUserNameList != null ? implUserNameList : new ArrayList<>()); + } + + xiSpeak.setApproveInfo(approveMap); + this.updateById(xiSpeak); + log.info("部门审批信息保存成功 mainId={}, deptId={}, isEnd={}, implWorker={}, implUserNameList={}", + mainId, deptId, isEnd, implWorker, implUserNameList); + } }