From 0bc279957f6f0df1611817a8e35e216ca9e5e024 Mon Sep 17 00:00:00 2001 From: new_new_new <8454518+new-twice@user.noreply.gitee.com> Date: Wed, 3 Jun 2026 09:22:12 +0000 Subject: [PATCH] =?UTF-8?q?!31=20feat(flow):=20=E6=96=B0=E5=A2=9E=E5=B7=A1?= =?UTF-8?q?=E8=A7=86=E6=95=B4=E6=94=B9=E5=92=8C=E5=B7=A1=E8=A7=86=E6=95=B4?= =?UTF-8?q?=E6=94=B9=E9=94=80=E5=8F=B7=E6=B5=81=E7=A8=8B=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(flow): 新增巡视整改和巡视整改销号流程模块 * feat(flow): 新增巡视整改和巡视整改销号流程 YAML 配置 * chore: 添加 localDocs 到 gitignore 并移除已跟踪的 docs 文件 --- .gitignore | 1 + docs/流程引擎架构分析.md | 423 ------------------ .../main/java/org/jeecg/FLOW-MODULE-AUDIT.md | 244 ++++++++++ .../main/java/org/jeecg/FLOW-MODULE-GUIDE.md | 139 ++++++ .../InspectCloseoutConfig.java | 35 ++ .../inspectcloseout/InspectCloseoutFlow.java | 132 ++++++ .../AfterCloseoutApprovedListener.java | 38 ++ .../inspectimprove/InspectImproveConfig.java | 39 ++ .../inspectimprove/InspectImproveFlow.java | 164 +++++++ .../AfterInspectImproveCompleteListener.java | 50 +++ .../src/main/resources/application-flow.yml | 22 + 11 files changed, 864 insertions(+), 423 deletions(-) delete mode 100644 docs/流程引擎架构分析.md create mode 100644 jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/FLOW-MODULE-AUDIT.md create mode 100644 jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/FLOW-MODULE-GUIDE.md create mode 100644 jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/inspectcloseout/InspectCloseoutConfig.java create mode 100644 jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/inspectcloseout/InspectCloseoutFlow.java create mode 100644 jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/inspectcloseout/listener/AfterCloseoutApprovedListener.java create mode 100644 jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/inspectimprove/InspectImproveConfig.java create mode 100644 jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/inspectimprove/InspectImproveFlow.java create mode 100644 jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/inspectimprove/listener/AfterInspectImproveCompleteListener.java diff --git a/.gitignore b/.gitignore index cc93aad..e345e9b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ **/.idea *.iml rebel.xml +localDocs ## backend **/target diff --git a/docs/流程引擎架构分析.md b/docs/流程引擎架构分析.md deleted file mode 100644 index bf38c08..0000000 --- a/docs/流程引擎架构分析.md +++ /dev/null @@ -1,423 +0,0 @@ -# 流程引擎架构分析 - -基于 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/FLOW-MODULE-AUDIT.md b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/FLOW-MODULE-AUDIT.md new file mode 100644 index 0000000..083294a --- /dev/null +++ b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/FLOW-MODULE-AUDIT.md @@ -0,0 +1,244 @@ +# 流程模块架构审计报告 + +> 审计范围:`org.jeecg` 下 4 个流程模块(xispeak / xispeakfb / inspectimprove / inspectcloseout) +> 共 18 个 Java 文件,含 4 个 Config、4 个 Flow、10 个 Listener + +## 一、模块概览 + +| 模块 | 文件数 | Flow 行数 | 业务完整度 | DI 健康度 | +|------|--------|-----------|-----------|-----------| +| xispeak | 9 (1C+1F+7L) | 432 | 完整 | 4/7 正常,3/7 有问题 | +| xispeakfb | 3 (1C+1F+1L) | 322 | 完整 | 正常 | +| inspectimprove | 3 (1C+1F+1L) | 164 | TODO 未完成 | 正常 | +| inspectcloseout | 3 (1C+1F+1L) | 132 | TODO 未完成 | 正常 | + +--- + +## 二、P0 致命问题 + +### 2.1 `StringUtil` 拼写错误 — 编译/运行时报错 + +**文件** `AfterSDWApproveHqListener.java` 第 50 行: + +```java +if(org.apache.commons.lang.StringUtil.isEmpty(url)) { +``` + +Apache Commons Lang 中不存在 `StringUtil` 类,应为 `StringUtils`。 + +**影响**:该行不会通过编译;如果通过(如 IDE 自动导入),运行时会抛出 `NoClassDefFoundError`。 + +### 2.2 链式调用直接 NPE(3 处) + +**`XiSpeakFlow.java`:** + +1. **行 166**:`xiSpeakConfig.getXiSpeak().getNeedSdwApproveCode()` — `getXiSpeak()` 可能返回 null(yml 缺失此段落时 Spring Boot 不会注入默认值,导致属性为 null) +2. **行 172**:`xiSpeakConfig.getXiSpeak().getFeedbackRightNowCode()` — 同上 +3. **行 178**:`xiSpeakConfig.getXiSpeak().getSdwLeaderListKey()` — 同上 + +同类方法 `getBgDeptLdUserIdList()` 行 37-41 做了守卫判断,但上述 3 处遗漏。 + +**`XiSpeakFlow.java` 行 310:** + +```java +public int getImplDeptIsEnd(DelegateExecution execution) { + return getDeptApproveDetail(execution).getIsEnd(); +} +``` + +`getDeptApproveDetail()` 可能返回 null(当 businessKey 为空、deptVar 为 null、entity 不存在时),直接调 `.getIsEnd()` 必抛 NPE。 + +### 2.3 `InspectImproveFlow` / `InspectCloseoutFlow` 所有公开方法配置 NPE + +```java +public List getMeasureResLeaderList(Object jsonData) { + InspectImproveConfig.InspectImproveProperties props = inspectImproveConfig.getInspectImprove(); + return getListFromData(jsonData, props.getMeasureResLeaderKey(), "..."); +} +``` + +`getInspectImprove()` 无 null 守卫。如果 yml 中 `inspect-improve` 或 `inspect-closeout` 配置节缺失,所有公开方法调用时接连抛出 NPE。 + +--- + +## 三、P1 高危问题 + +### 3.1 JSON 解析方法大面织重复(~180 行) + +同一个 "JSONObject / JSONString / POJO 三种类型兼容解析" 逻辑在以下 7 个方法中独立实现: + +| 类 | 方法 | 返回类型 | +|----|------|----------| +| `XiSpeakFlow` | `getCodeValueFromData` | `Integer` | +| `XiSpeakFlow` | `getListCodeValueFromData` | `List` | +| `XiSpeakFlow` | `getImplDeptNums` | `Integer` (变体) | +| `InspectImproveFlow` | `getStringFromData` | `String` | +| `InspectImproveFlow` | `getListFromData` | `List` | +| `InspectCloseoutFlow` | `getStringFromData` | `String` | +| `InspectCloseoutFlow` | `getListFromData` | `List` | + +**建议**:抽取为 `FlowJsonParser` 工具类,提供 `getString(jsonData, key)` / `getInteger(jsonData, key, default)` / `getStringList(jsonData, key)` 三个通用方法。 + +### 3.2 2 级父 Execution 攀爬逻辑三重复制(~135 行) + +三个 Listener 中有逐行相同的 45 行核心逻辑,仅变量名不同: + +| Listener | 存储变量名 | +|----------|-----------| +| `AfterImplDeptLeaderApproveListener` | 硬编码 `"tem_impl_leader"` | +| `AfterImplWorkerTemStoreListener` | 硬编码 `"tem_impl_worker"` | +| `AfterBgLeaderApproveListener` | 配置 `props.getTemBgLeaderKey()` | + +**建议**:提取抽象模板方法,参数化变量名和日志前缀。 + +### 3.3 `getListSize()` 重复定义(3 处) + +`XiSpeakFlow` 行 340、`InspectImproveFlow` 行 155、`InspectCloseoutFlow` 行 123 都有语义完全相同的实现。 + +**建议**:移入共用工具类。 + +### 3.4 依赖注入混乱(3 个 Listener) + +| Listener | 注入方式 | 问题 | +|----------|---------|------| +| `AfterSDWApproveHqListener` | `@RequiredArgsConstructor`(2 final) **+** `@Autowired` 字段注入 `RuntimeService` | 混合注入,风格撕裂 | +| `AfterImplDeptLeaderApproveListener` | `@RequiredArgsConstructor` 零 final + `static { SpringContextUtils.getBean(...) }` | 注解虚设,static 块获取 Bean 有容器未就绪风险 | +| `AfterImplWorkerTemStoreListener` | 同上 | 同上 | + +**建议**:统一为 `@RequiredArgsConstructor(onConstructor_ = @Autowired)` + private final 字段。 + +### 3.5 事务注解缺失 + +`AfterBgWorkerFinalApproveListener` 执行 `bgXiSpeakService.updateById(entity)` 却**没有** `@Transactional`。 + +虽然 MyBatis-Plus 的 `updateById` 自身在一条 SQL 中完成,但如果后续有人在此方法中增加第二个 DB 操作,不会感知到缺少事务边界。 + +### 3.6 悲观锁 vs 无锁策略不一致 + +| Listener | 读方法 | 锁 | +|----------|--------|----| +| `AfterImplWorkerApproveListener` | `getByIdForUpdate` | 悲观锁 (SELECT FOR UPDATE) | +| `AfterImplDeptLeaderStoreJsonListener` | `getById` | 无锁 | +| xispeakfb `AfterImplDeptLeaderApproveListener` | `getById` | 无锁 | + +多实例并行会签场景下,无锁的 `getById` →修改→ `updateById` 存在**丢失更新**风险。 + +--- + +## 四、P2 中危问题 + +### 4.1 `parseDeptId` 两个版本功能不对等 + +- **xispeak 版** (`AfterImplDeptLeaderStoreJsonListener` 行 115-137):处理 null / Collection / 普通字符串 / `[1001,1002]` 数组字符串 +- **xispeakfb 版** (`AfterImplDeptLeaderApproveListener` 行 115-123):仅处理 Collection / 普通字符串,缺失数组字符串兼容 + +如果 xispeakfb 流程中传入 `[deptA, deptB]` 格式的变量值(这在 xispeak 流程中很常见),解析结果会包含方括号导致后续匹配失败。 + +### 4.2 流程变量 fallback 模式重复(4 处) + +同一 "先 `getVariableLocal` 再 `getVariable`" 的 4 行模式出现在 4 个不同位置,无统一封装。 + +### 4.3 `AfterSDWApproveHqListener` 日志硬编码字段名 + +行 80 的日志 `"解析 JSON 字段 pending_depts_id 失败"` 硬编码了特定字段名,与运行时通过 `props.getImplDeptKey()` 读取的实际字段名可能不一致,降低排查效率。 + +### 4.4 StringUtils 库引用混乱 + +| 类 | 使用的 StringUtils | +|----|--------------------| +| `XiSpeakFlow` | `org.apache.shiro.util.StringUtils`、`org.apache.commons.lang.StringUtils`、`org.apache.commons.lang3.StringUtils` — 三个库混用 | +| `XiSpeakFeedbackFlow` | `org.apache.commons.lang3.StringUtils` 统一 | +| `InspectImproveFlow` | `org.apache.commons.lang.StringUtils` (lang) | +| `InspectCloseoutFlow` | `org.apache.commons.lang.StringUtils` (lang) | +| 各 Listener | lang 和 lang3 混用 | + +同一类中三种 `StringUtils` 混用增加混淆。Apache Commons Lang (`lang`) 是旧版,`lang3` 是新版,`shiro` 是第三方。 + +--- + +## 五、P3 低危问题 + +### 5.1 两个新的 Listener 含 TODO 未实现 + +- `AfterInspectImproveCompleteListener` 行 44:只输出日志,未更新业务表 +- `AfterCloseoutApprovedListener` 行 35:同上 + +### 5.2 大量未使用的 Import + +约 7 个 Listener 包含从模板复刻过来的未使用 import(如 `LambdaQueryWrapper`、`FlowNodeExpression`、`WorkFlowGlobals`、`TaskTask`、`ITaskTaskService`),推测是从其他文件复制后未清理。 + +### 5.3 缺少 `@Override` + +xispeakfb `AfterImplDeptLeaderApproveListener` 的 `notify()` 方法缺少 `@Override`,失去编译期接口契约校验。 + +### 5.4 硬编码变量名 vs 配置化 + +`AfterImplDeptLeaderApproveListener` 和 `AfterImplWorkerTemStoreListener` 分别硬编码 `"tem_impl_leader"` 和 `"tem_impl_worker"`,而 `AfterBgLeaderApproveListener` 正确地通过 Config 读取。行为不一致。 + +### 5.5 `getImplDeptLeader()` 逻辑矛盾 + +```java +// XiSpeakFlow 行 246-252 +public String getImplDeptLeader(DelegateExecution execution) { + DeptApproveDetail detail = getDeptApproveDetail(execution); + if (detail != null && detail.getIsEnd() != null) { + return String.valueOf(detail.getImplWorker()); // isEnd 非 null → 取 implWorker + } + return detail == null ? "" : detail.getImplWorker(); // isEnd 为 null → 也取 implWorker +} +``` + +两个分支返回同一个字段,`isEnd` 判断形同虚设(可能是条件写反)。 + +--- + +## 六、架构演进建议 + +### 6.1 短期(本迭代) + +1. 修复 4 处 P0 致命问题(`StringUtil` 拼写、3 处 NPE) +2. 统一 `AfterSDWApproveHqListener` 注入方式 +3. 统一两处错误注入(static 块 → 构造注入) +4. 补上 `AfterInspectImproveCompleteListener` 和 `AfterCloseoutApprovedListener` 的 TODO + +### 6.2 中期(下个迭代) + +5. 将 `getStringFromData` / `getListFromData` / `getListSize` 提取为共用 `FlowJsonParser` 工具类 +6. 将 2 级父 Execution 攀爬逻辑提取为可复用的模板方法或工具方法 +7. 统一 `parseDeptId` 到完整版 +8. 统一 `StringUtils` 引用为 `org.apache.commons.lang3.StringUtils` +9. 清理未使用的 import + +### 6.3 长期(架构层面) + +10. 考虑引入流程模块 SDK 层(`flow-module-sdk`),包含: + - 基类 `AbstractFlowConfig` / `AbstractFlowExpression` + - 泛型 JSON 解析器 + - 通用 Listener 模板(变量攀爬、审批记录持久化) +11. 统一事务策略文档:哪些 Listener 需要 `@Transactional`,何时用悲观锁 vs 乐观锁 +12. 建立模块创建 CheckList,新模块上线前逐项验证(DI 一致性、NPE 防御、事务标注、无硬编码等) + +--- + +## 七、CheckList:新流程模块自查项 + +| # | 检查项 | 说明 | +|---|--------|------| +| 1 | Config 注入方式 | `@Component` + `@ConfigurationProperties(prefix = "flow-biz")` | +| 2 | Config 内嵌类 | 属性名与 yml key 中划线自动映射驼峰 | +| 3 | Flow 注入方式 | `@RequiredArgsConstructor(onConstructor_ = @Autowired)` + private final | +| 4 | Flow Bean 命名 | `@Component("xxxFlow")`,避免与其它模块冲突 | +| 5 | 所有公开方法守卫 null | Flow 方法中检查 `getXxx()` 返回 null | +| 6 | 是否复用 `getStringFromData` / `getListFromData` | 优先使用共用工具类,不复写 | +| 7 | Listener 注入方式 | 统一构造注入,禁止 static 块和字段注入 | +| 8 | Listener 类型选择 | 需在任务创建前初始化变量 → ExecutionListener;任务完成回调 → TaskListener | +| 9 | 事务注解 | 有 DB 写操作加 `@Transactional(rollbackFor = Exception.class)` | +| 10 | 锁策略 | 并发写同一行 → `getByIdForUpdate`;无并发 → `getById` | +| 11 | 异常处理策略 | 需阻断流程 → rethrow;可容忍 → catch + log + 返回默认值 | +| 12 | `StringUtils` | 统一使用 `org.apache.commons.lang3.StringUtils` | +| 13 | 无硬编码变量名 | 变量名统一从 Config 读取 | +| 14 | 无未使用 import | 提交前 IDE 自动优化 imports | +| 15 | `@Override` | 所有 `notify()` 方法加 `@Override` | +| 16 | yml 配置节 | 与 Config 内嵌类属性一一对应 | +| 17 | 无 TODO | 功能完整的 Listener 不应留 TODO | +| 18 | 日志规范 | 异常用 `log.error`、业务用 `log.info`、跳过/降级用 `log.warn` | diff --git a/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/FLOW-MODULE-GUIDE.md b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/FLOW-MODULE-GUIDE.md new file mode 100644 index 0000000..c17319b --- /dev/null +++ b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/FLOW-MODULE-GUIDE.md @@ -0,0 +1,139 @@ +# 流程模块开发指南 + +本文档说明如何为业务模块创建对应的流程集成包,基于 `org.jeecg.xispeak` 的设计模式。 + +## 目录结构 + +``` +org/jeecg// +├── XxxConfig.java # 配置类,映射 YAML +├── XxxFlow.java # 流程表达式 Bean +└── listener/ + ├── AfterXxxListener.java # TaskListener / ExecutionListener + └── ... +``` + +## 三步创建流程模块 + +### Step 1: 创建 Config 配置类 + +读取 `application-flow.yml` 中 `flow-biz.` 段落。 + +```java +@Data +@Component +@ConfigurationProperties(prefix = "flow-biz") +public class XxxConfig { + private XxxProperties xxx; // 对应 yml 中 xxx 节点 + + @Data + public static class XxxProperties { + private String businessKey; // 业务主键变量名 + private String jsonDataKey; // JSON 数据变量名 + private String formUrl; // 业务表单路径 + private String flowCode; // 流程定义编码(可选) + // ... 按需添加字段名配置 + } +} +``` + +**要点:** +- 外层类 `@ConfigurationProperties(prefix = "flow-biz")`,内层属性名对应 YAML 键名 +- Spring 自动做中划线 → 驼峰映射(`business-key` → `businessKey`) +- 将业务字段名配置化,避免硬编码,方便流程设计调整 + +### Step 2: 创建 Flow 表达式 Bean + +供 Flowable 流程表达式 `${xxxFlow.method(args)}` 调用。 + +```java +@Slf4j +@Component("xxxFlow") // Bean 名即流程表达式前缀 +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class XxxFlow { + + private final XxxConfig xxxConfig; + // 注入需要的 Service + + // 提供流程表达式方法 + public List getApproverList(Object jsonData) { ... } + public int getApproverListLength(Object jsonData) { ... } +} +``` + +**常用方法模式:** +| 方法 | 用途 | 表达式示例 | +|------|------|-----------| +| `getXxxList(jsonData)` | 从 JSON 解析列表 | `${xxxFlow.getApproverList(json_data)}` | +| `getXxxListLength(jsonData)` | 返回列表长度 | `${xxxFlow.getApproverListLength(json_data) > 0}` | +| `getXxx(jsonData)` | 从 JSON 解析单值 | `${xxxFlow.getApplicant(json_data)}` | +| `getListSize(str)` | 逗号分隔字符串计数 | `${xxxFlow.getListSize(someVar)}` | + +**要点:** +- Bean 名要有命名空间意识,不可与其它模块冲突 +- 所有方法需防御 null/空字符串输入 +- Length 类方法用于流程网关条件判断 + +### Step 3: 创建 Listener + +监听流程节点事件,执行业务回调。 + +```java +@Slf4j +@Component("AfterXxxListener") // Bean 名供 Flowable 设计器引用 +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class AfterXxxListener implements TaskListener { + + private final XxxConfig xxxConfig; + + @Override + public void notify(DelegateTask delegateTask) { + // 1. 读取流程变量 + // 2. 执行业务逻辑 + // 3. 异常时慎重决定是否 rethrow + } +} +``` + +**Listener 类型:** +- `TaskListener` — 用户任务完成/创建时触发 +- `ExecutionListener` — 流程实例/节点生命周期事件 + +**要点:** +- 必须在 `notify` 开头做 null 防御(assignee、变量等可能为空) +- 需要读写业务表时加 `@Transactional(rollbackFor = Exception.class)` +- 异常处理:阻断流程 rethrow,不阻断 catch 后 log + +### Step 4: 添加 YAML 配置 + +在 `application-flow.yml` 的 `flow-biz` 下新增配置节: + +```yaml +flow-biz: + : + business-key: "business_id" + json-data-key: "json_data" + form-url: "path/to/BPMForm" + flow-code: "process_xxx" + # ... 模块专属字段 +``` + +## 已有模块速查 + +| 模块 | 包路径 | YAML 节点 | Flow Bean | 对应业务实体 | +|------|--------|-----------|-----------|-------------| +| 习讲话 | `org.jeecg.xispeak` | `xi-speak` | `xiSpeakFlow` | `BgXiSpeak` | +| 巡视整改 | `org.jeecg.inspectimprove` | `inspect-improve` | `inspectImproveFlow` | `DjInspectImprove` | +| 巡视整改销号 | `org.jeecg.inspectcloseout` | `inspect-closeout` | `inspectCloseoutFlow` | 待定 | + +## 流程表达式调试 + +在 Flowable 流程设计器中,表达式写法为 `${beanName.methodName(args)}`: + +``` +${xiSpeakFlow.getNeedSdwApproval(json_data) == 1} +${inspectImproveFlow.getMeasureResLeaderListLength(json_data) > 0} +${inspectCloseoutFlow.getApproverListLength(json_data) == 0} +``` + +`json_data` 变量由子流程启动监听器(如 `AfterSDWApproveHqListener`)从主流程注入,具体变量名见各模块 YAML 的 `json-data-key`。 diff --git a/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/inspectcloseout/InspectCloseoutConfig.java b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/inspectcloseout/InspectCloseoutConfig.java new file mode 100644 index 0000000..99f51e8 --- /dev/null +++ b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/inspectcloseout/InspectCloseoutConfig.java @@ -0,0 +1,35 @@ +package org.jeecg.inspectcloseout; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "flow-biz") +public class InspectCloseoutConfig { + + private InspectCloseoutProperties inspectCloseout; + + @Data + public static class InspectCloseoutProperties { + /** 业务主键 key */ + private String businessKey; + /** JSON 数据 key */ + private String jsonDataKey; + /** 表单 URL */ + private String formUrl; + /** 流程编码 */ + private String flowCode; + /** 销号申请人 key */ + private String applicantKey; + /** 销号审批人 key */ + private String approverKey; + /** 整改事项关联 key */ + private String rectifyItemKey; + /** 销号说明 key */ + private String closeoutDescKey; + /** 完成状态 key */ + private String completionStatusKey; + } +} diff --git a/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/inspectcloseout/InspectCloseoutFlow.java b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/inspectcloseout/InspectCloseoutFlow.java new file mode 100644 index 0000000..8ca5d22 --- /dev/null +++ b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/inspectcloseout/InspectCloseoutFlow.java @@ -0,0 +1,132 @@ +package org.jeecg.inspectcloseout; + +import com.alibaba.fastjson.JSONObject; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.jeecg.modules.extbpm.process.common.expression.FlowNodeExpression; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@Component("inspectCloseoutFlow") +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class InspectCloseoutFlow { + + private final InspectCloseoutConfig inspectCloseoutConfig; + private final FlowNodeExpression flowNodeExpression; + + // =================================================================== + // 通用工具方法 + // =================================================================== + + private String getStringFromData(Object jsonData, String codeName, String logLabel) { + if (jsonData == null || StringUtils.isBlank(codeName)) { + return null; + } + try { + JSONObject data; + if (jsonData instanceof JSONObject) { + data = (JSONObject) jsonData; + } else if (jsonData instanceof String) { + String jsonStr = (String) jsonData; + if (StringUtils.isBlank(jsonStr)) return null; + data = JSONObject.parseObject(jsonStr); + } else { + data = JSONObject.parseObject(JSONObject.toJSONString(jsonData)); + } + String result = data.getString(codeName); + log.info("{} 解析结果: {}", logLabel, result); + return result; + } catch (Exception e) { + log.warn("JSON解析失败: label={}, key={}", logLabel, codeName, e); + return null; + } + } + + private List getListFromData(Object jsonData, String codeName, String logLabel) { + if (jsonData == null || StringUtils.isBlank(codeName)) { + return Collections.emptyList(); + } + try { + JSONObject data; + if (jsonData instanceof JSONObject) { + data = (JSONObject) jsonData; + } else if (jsonData instanceof String) { + String jsonStr = (String) jsonData; + if (StringUtils.isBlank(jsonStr)) return Collections.emptyList(); + data = JSONObject.parseObject(jsonStr); + } else { + data = JSONObject.parseObject(JSONObject.toJSONString(jsonData)); + } + String rawValue = data.getString(codeName); + if (StringUtils.isBlank(rawValue)) { + return Collections.emptyList(); + } + List result = Arrays.stream(rawValue.split(",")) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toList()); + log.info("{} 解析结果: {}", logLabel, result); + return result; + } catch (Exception e) { + log.warn("JSON解析失败: label={}, key={}", logLabel, codeName, e); + return Collections.emptyList(); + } + } + + // =================================================================== + // 流程表达式方法 — 用法示例: ${inspectCloseoutFlow.xxx} + // =================================================================== + + /** + * ${inspectCloseoutFlow.getApproverList(json_data)} + * 获取销号审批人列表 + */ + public List getApproverList(Object jsonData) { + InspectCloseoutConfig.InspectCloseoutProperties props = inspectCloseoutConfig.getInspectCloseout(); + return getListFromData(jsonData, props.getApproverKey(), "销号审批人"); + } + + /** + * ${inspectCloseoutFlow.getApproverListLength(json_data)} + */ + public int getApproverListLength(Object jsonData) { + return getApproverList(jsonData).size(); + } + + /** + * ${inspectCloseoutFlow.getApplicant(json_data)} + * 获取销号申请人 + */ + public String getApplicant(Object jsonData) { + InspectCloseoutConfig.InspectCloseoutProperties props = inspectCloseoutConfig.getInspectCloseout(); + return getStringFromData(jsonData, props.getApplicantKey(), "销号申请人"); + } + + /** + * ${inspectCloseoutFlow.getRectifyItemId(json_data)} + * 获取关联的整改事项ID + */ + public String getRectifyItemId(Object jsonData) { + InspectCloseoutConfig.InspectCloseoutProperties props = inspectCloseoutConfig.getInspectCloseout(); + return getStringFromData(jsonData, props.getRectifyItemKey(), "整改事项ID"); + } + + /** + * ${inspectCloseoutFlow.getListSize(data)} + * 计算逗号分隔字符串的元素个数 + */ + public int getListSize(String data) { + if (StringUtils.isEmpty(data)) { + return 0; + } + return (int) Arrays.stream(data.split(",")) + .map(String::trim) + .filter(StringUtils::isNotEmpty) + .count(); + } +} diff --git a/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/inspectcloseout/listener/AfterCloseoutApprovedListener.java b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/inspectcloseout/listener/AfterCloseoutApprovedListener.java new file mode 100644 index 0000000..bb0f00c --- /dev/null +++ b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/inspectcloseout/listener/AfterCloseoutApprovedListener.java @@ -0,0 +1,38 @@ +package org.jeecg.inspectcloseout.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.delegate.TaskListener; +import org.flowable.task.service.delegate.DelegateTask; +import org.jeecg.inspectcloseout.InspectCloseoutConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Objects; + +/** + * 巡视整改销号审批通过后,更新关联整改事项状态 + */ +@Slf4j +@Component("AfterCloseoutApprovedListener") +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class AfterCloseoutApprovedListener implements TaskListener { + + private static final long serialVersionUID = 1L; + private final InspectCloseoutConfig inspectCloseoutConfig; + + @Override + public void notify(DelegateTask delegateTask) { + Object businessKeyObj = delegateTask.getVariable( + inspectCloseoutConfig.getInspectCloseout().getBusinessKey()); + String businessKey = Objects.toString(businessKeyObj, "").trim(); + + if (businessKey.isEmpty()) { + log.warn("【销号审批监听器】未查询到业务表单ID,跳过。TaskId: {}", delegateTask.getId()); + return; + } + + // TODO: 根据实际业务实体,更新关联的整改事项状态为"已销号" + log.info("【销号审批监听器】业务表单 [id={}] 销号审批通过", businessKey); + } +} diff --git a/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/inspectimprove/InspectImproveConfig.java b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/inspectimprove/InspectImproveConfig.java new file mode 100644 index 0000000..508e1fd --- /dev/null +++ b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/inspectimprove/InspectImproveConfig.java @@ -0,0 +1,39 @@ +package org.jeecg.inspectimprove; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "inspect-improve") +public class InspectImproveConfig { + + private InspectImproveProperties inspectImprove; + + @Data + public static class InspectImproveProperties { + /** 业务主键 key */ + private String businessKey; + /** JSON 数据 key */ + private String jsonDataKey; + /** 表单 URL */ + private String formUrl; + /** 流程编码 */ + private String flowCode; + /** 措施责任领导 key */ + private String measureResLeaderKey; + /** 措施责任部门 key */ + private String measureResDeptKey; + /** 问题责任领导 key */ + private String questionResLeaderKey; + /** 问题责任部门 key */ + private String questionResDeptKey; + /** 整改措施 key */ + private String improveMeasureKey; + /** 工作进展 key */ + private String workProgressKey; + /** 完成状态 key */ + private String completionStatusKey; + } +} diff --git a/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/inspectimprove/InspectImproveFlow.java b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/inspectimprove/InspectImproveFlow.java new file mode 100644 index 0000000..ac62420 --- /dev/null +++ b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/inspectimprove/InspectImproveFlow.java @@ -0,0 +1,164 @@ +package org.jeecg.inspectimprove; + +import com.alibaba.fastjson.JSONObject; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.jeecg.modules.dj.inspectimprove.entity.DjInspectImprove; +import org.jeecg.modules.dj.inspectimprove.service.IDjInspectImproveService; +import org.jeecg.modules.extbpm.process.common.expression.FlowNodeExpression; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@Component("inspectImproveFlow") +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class InspectImproveFlow { + + private final InspectImproveConfig inspectImproveConfig; + private final IDjInspectImproveService djInspectImproveService; + private final FlowNodeExpression flowNodeExpression; + + // =================================================================== + // 通用工具方法 + // =================================================================== + + /** + * 从 JSON 数据中解析字符串字段 + */ + private String getStringFromData(Object jsonData, String codeName, String logLabel) { + if (jsonData == null || StringUtils.isBlank(codeName)) { + return null; + } + try { + JSONObject data; + if (jsonData instanceof JSONObject) { + data = (JSONObject) jsonData; + } else if (jsonData instanceof String) { + String jsonStr = (String) jsonData; + if (StringUtils.isBlank(jsonStr)) return null; + data = JSONObject.parseObject(jsonStr); + } else { + data = JSONObject.parseObject(JSONObject.toJSONString(jsonData)); + } + String result = data.getString(codeName); + log.info("{} 解析结果: {}", logLabel, result); + return result; + } catch (Exception e) { + log.warn("JSON解析失败: label={}, key={}", logLabel, codeName, e); + return null; + } + } + + /** + * 从 JSON 数据中解析逗号分隔的列表字段 + */ + private List getListFromData(Object jsonData, String codeName, String logLabel) { + if (jsonData == null || StringUtils.isBlank(codeName)) { + return Collections.emptyList(); + } + try { + JSONObject data; + if (jsonData instanceof JSONObject) { + data = (JSONObject) jsonData; + } else if (jsonData instanceof String) { + String jsonStr = (String) jsonData; + if (StringUtils.isBlank(jsonStr)) return Collections.emptyList(); + data = JSONObject.parseObject(jsonStr); + } else { + data = JSONObject.parseObject(JSONObject.toJSONString(jsonData)); + } + String rawValue = data.getString(codeName); + if (StringUtils.isBlank(rawValue)) { + return Collections.emptyList(); + } + List result = Arrays.stream(rawValue.split(",")) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toList()); + log.info("{} 解析结果: {}", logLabel, result); + return result; + } catch (Exception e) { + log.warn("JSON解析失败: label={}, key={}", logLabel, codeName, e); + return Collections.emptyList(); + } + } + + // =================================================================== + // 流程表达式方法 — 用法示例: ${inspectImproveFlow.xxx} + // =================================================================== + + /** + * ${inspectImproveFlow.getMeasureResLeaderList(json_data)} + * 获取措施责任领导列表 + */ + public List getMeasureResLeaderList(Object jsonData) { + InspectImproveConfig.InspectImproveProperties props = inspectImproveConfig.getInspectImprove(); + return getListFromData(jsonData, props.getMeasureResLeaderKey(), "措施责任领导"); + } + + /** + * ${inspectImproveFlow.getMeasureResLeaderListLength(json_data)} + */ + public int getMeasureResLeaderListLength(Object jsonData) { + return getMeasureResLeaderList(jsonData).size(); + } + + /** + * ${inspectImproveFlow.getQuestionResLeaderList(json_data)} + * 获取问题责任领导列表 + */ + public List getQuestionResLeaderList(Object jsonData) { + InspectImproveConfig.InspectImproveProperties props = inspectImproveConfig.getInspectImprove(); + return getListFromData(jsonData, props.getQuestionResLeaderKey(), "问题责任领导"); + } + + /** + * ${inspectImproveFlow.getQuestionResLeaderListLength(json_data)} + */ + public int getQuestionResLeaderListLength(Object jsonData) { + return getQuestionResLeaderList(jsonData).size(); + } + + /** + * ${inspectImproveFlow.getMeasureResDeptList(json_data)} + * 获取措施责任部门列表 + */ + public List getMeasureResDeptList(Object jsonData) { + InspectImproveConfig.InspectImproveProperties props = inspectImproveConfig.getInspectImprove(); + return getListFromData(jsonData, props.getMeasureResDeptKey(), "措施责任部门"); + } + + /** + * ${inspectImproveFlow.getMeasureResDeptListLength(json_data)} + */ + public int getMeasureResDeptListLength(Object jsonData) { + return getMeasureResDeptList(jsonData).size(); + } + + /** + * ${inspectImproveFlow.getMeasureResLeader(json_data)} + * 获取措施责任领导(单值) + */ + public String getMeasureResLeader(Object jsonData) { + InspectImproveConfig.InspectImproveProperties props = inspectImproveConfig.getInspectImprove(); + return getStringFromData(jsonData, props.getMeasureResLeaderKey(), "措施责任领导"); + } + + /** + * ${inspectImproveFlow.getListSize(json_data)} + * 计算逗号分隔字符串的元素个数 + */ + public int getListSize(String data) { + if (StringUtils.isEmpty(data)) { + return 0; + } + return (int) Arrays.stream(data.split(",")) + .map(String::trim) + .filter(StringUtils::isNotEmpty) + .count(); + } +} diff --git a/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/inspectimprove/listener/AfterInspectImproveCompleteListener.java b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/inspectimprove/listener/AfterInspectImproveCompleteListener.java new file mode 100644 index 0000000..bbb8b2f --- /dev/null +++ b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/inspectimprove/listener/AfterInspectImproveCompleteListener.java @@ -0,0 +1,50 @@ +package org.jeecg.inspectimprove.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.delegate.TaskListener; +import org.flowable.task.service.delegate.DelegateTask; +import org.jeecg.modules.dj.inspectimprove.entity.DjInspectImprove; +import org.jeecg.modules.dj.inspectimprove.service.IDjInspectImproveService; +import org.jeecg.inspectimprove.InspectImproveConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Objects; + +/** + * 巡视整改流程最终审批通过后,将业务表单状态标记为已完成 + */ +@Slf4j +@Component("AfterInspectImproveCompleteListener") +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class AfterInspectImproveCompleteListener implements TaskListener { + + private static final long serialVersionUID = 1L; + private final InspectImproveConfig inspectImproveConfig; + private final IDjInspectImproveService djInspectImproveService; + + @Override + public void notify(DelegateTask delegateTask) { + Object businessKeyObj = delegateTask.getVariable( + inspectImproveConfig.getInspectImprove().getBusinessKey()); + String businessKey = Objects.toString(businessKeyObj, "").trim(); + + if (businessKey.isEmpty()) { + log.warn("【巡视整改完成监听器】未查询到业务表单ID,跳过。TaskId: {}", delegateTask.getId()); + return; + } + + try { + DjInspectImprove entity = djInspectImproveService.getById(businessKey); + if (entity == null) { + log.warn("【巡视整改完成监听器】业务数据不存在,id: {}", businessKey); + return; + } + // TODO: 根据实际业务补充完成状态字段的更新逻辑 + log.info("【巡视整改完成监听器】业务表单 [id={}] 流程已完成", businessKey); + } catch (Exception e) { + log.error("【巡视整改完成监听器】更新业务表单异常, businessKey: " + businessKey, e); + } + } +} 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 77f666a..912d688 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 @@ -18,6 +18,28 @@ flow-biz: tem-bg-leader-key: "tem_bg_leader" sdw-leader-list-key: "sdwLeaderList" is-end-key: "isEnd" + inspect-improve: + business-key: "business_id" + json-data-key: "json_data" + form-url: "dj/inspectimprove/components/DjInspectImproveBPMForm" + flow-code: "" + measure-res-leader-key: "measureResLeader" + measure-res-dept-key: "measureResDept" + question-res-leader-key: "questionResLeader" + question-res-dept-key: "questionResDept" + improve-measure-key: "improveMeasure" + work-progress-key: "workProgress" + completion-status-key: "completionStatus" + inspect-closeout: + business-key: "business_id" + json-data-key: "json_data" + form-url: "dj/inspectcloseout/components/InspectCloseoutBPMForm" + flow-code: "" + applicant-key: "applicant" + approver-key: "approver" + rectify-item-key: "rectifyItemId" + closeout-desc-key: "closeoutDesc" + completion-status-key: "completionStatus" xi-speak-fb: dept-id: "x" business-id: "business_id"