!28 feat(xispeak): 部门审批信息改为持久化到业务表单JSON,不再依赖流程引擎变量

Merge pull request !28 from new_new_new/feature/20260527
This commit is contained in:
new_new_new
2026-05-27 12:30:20 +00:00
committed by Gitee
13 changed files with 769 additions and 22 deletions
+423
View File
@@ -0,0 +1,423 @@
# 流程引擎架构分析
基于 Flowable 6.8.0Jeecg 在之上封装了自定义的流程元数据管理层,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
<sequenceFlow sourceRef="Gateway_01" targetRef="Task_02">
<conditionExpression>${xiSpeakFlow.getNeedSdwApproval(jsonData)}</conditionExpression>
</sequenceFlow>
```
业务监听器在 BPMN XML 中以全限定类名注册:
```xml
<flowable:taskListener event="complete"
class="org.jeecg.xispeak.listener.AfterImplDeptLeaderApproveListener"/>
```
---
## 四、特殊流程能力
| 能力 | 实现方式 | 关键类 |
|------|---------|--------|
| **会签** | Flowable `multiInstanceLoopCharacteristics`,运行时 XML 解析判断 | `SuperTaskService.checkUserTaskIsHuiQian()` |
| **加签** | Flowable `Command<Void>` 动态修改 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 表达式更复杂的动态计算
@@ -42,5 +42,6 @@ public class XiSpeakConfig {
private String temImplLeaderKey;
private String temBgLeaderKey;
private String sdwLeaderListKey;
private String isEndKey;
}
}
@@ -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<String> 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<String> 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<String> 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<String> 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());
}
}
@@ -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);
}
/**
* 获取当前流程的经办部门的经办人。
* <p>用于流程表达式判断,通过 BusinessKey 关联业务主键,获取当前经办人。</p>
@@ -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"
@@ -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<String> 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 {
@@ -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);
}
@@ -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);
}
}
@@ -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<Execution> 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);
@@ -266,6 +266,26 @@ public class BgXiSpeakController extends JeecgController<BgXiSpeak, IBgXiSpeakSe
return Result.OK("保存成功");
}
/**
* 保存部门审批信息(isEnd、部门经办人)到业务表单 approveInfo JSON
*/
@AutoLog(value = "保存部门审批信息")
@Operation(summary="保存部门审批信息到业务表单")
@PostMapping(value = "/saveSubApprove")
public Result<String> saveSubApprove(@RequestBody Map<String, Object> 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<String> implUserNameList = (List<String>) 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
@@ -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<String> implUserIdList; // 落实人Id
@JsonAlias("implUserName")
@@ -85,4 +85,10 @@ public interface IBgXiSpeakService extends IService<BgXiSpeak> {
List<BgXiSpeak> 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<String> implUserNameList);
}
@@ -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<BgXiSpeakMapper, BgXiSpeak
log.info("业务表单 {} 的所有流程已结束,完成状态已更新为已完成", businessId);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void saveSubApprove(String mainId, String deptId, Integer isEnd, String implWorker, List<String> 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);
}
}