diff --git a/.claude/agents/backend-common-infrastructure.md b/.claude/agents/backend-common-infrastructure.md index 3e1c3d60f..7b78e289b 100644 --- a/.claude/agents/backend-common-infrastructure.md +++ b/.claude/agents/backend-common-infrastructure.md @@ -11,6 +11,7 @@ description: 公共基础模块专家。用于修改 ruoyi-common 下的 mybatis 2. 优先保持公开方法签名、泛型、返回值、异常语义兼容。 3. 新增能力要贴合已有命名和链式调用风格,不自造平行体系。 4. 只补注释时不改实现、不重排 import、不运行无关格式化。 +5. 新增工具类或通用辅助方法前,先读取 `.claude/agents/backend-engineering.md` 的“已有能力复用”规则,按能力类别检索、确认模块依赖可用性并核对业务语义;满足需求时直接复用,不添加无业务语义的包装。 ## common-mybatis @@ -38,5 +39,5 @@ description: 公共基础模块专家。用于修改 ruoyi-common 下的 mybatis - 是否破坏已有调用点。 - 是否遗漏 `instance()` / `clear()` / 批量翻译 / 缓存失效等公共模块关键路径。 -- 是否新增了与现有工具重复的临时类或临时方法。 +- 是否已检索同模块、`ruoyi-common` 和已有依赖,避免重复能力;确需新增时,是否在交付说明中简述现有能力为何不适用(无需为此额外添加代码注释,既有 JavaDoc 规则仍适用)。 - JavaDoc 是否简洁说明公共 API 的参数、返回值和兼容语义。 diff --git a/.claude/agents/backend-crud.md b/.claude/agents/backend-crud.md index 87565ea67..dcf9d366a 100644 --- a/.claude/agents/backend-crud.md +++ b/.claude/agents/backend-crud.md @@ -11,6 +11,7 @@ description: 标准后端 CRUD 专家。用于当前项目中的新增单表 CRU 2. 再参考当前模块内最近似的标准管理模块。 3. 分层保持稳定: `domain`、`domain.bo`、`domain.vo`、`mapper`、`service`、`service.impl`、`controller` +4. 新增工具类或通用辅助方法前,先读取 `.claude/agents/backend-engineering.md` 的“已有能力复用”规则,按能力类别检索、确认模块依赖可用性并核对业务语义;满足需求时直接复用,不添加无业务语义的包装。 ## 结构约定 @@ -57,6 +58,7 @@ description: 标准后端 CRUD 专家。用于当前项目中的新增单表 CRU ## 自检 +- 新增工具类或通用辅助方法是否已完成同类能力检索与语义核对;确需新增时,在交付说明中简述现有能力为何不适用,无需为此额外添加代码注释,既有 JavaDoc 规则仍适用 - CRUD 链路是否完整 - BO / VO / Entity 是否职责分离 - 导出、分页、删除前校验是否齐全 diff --git a/.claude/agents/backend-engineering.md b/.claude/agents/backend-engineering.md index d442a7be3..2d394bbee 100644 --- a/.claude/agents/backend-engineering.md +++ b/.claude/agents/backend-engineering.md @@ -14,19 +14,32 @@ description: 后端工程总入口。用于在当前 RuoYi-Vue-Plus 项目中识 5. 如果只要求补充或修正 JavaDoc 注释,优先使用 `backend-javadoc.md` 的规则。 6. 如果同时要求同步前端接口或前端页面骨架,先确认目标前端是 Vue 还是 React,保持后端路由与 generator 风格稳定,便于前端 agent 对接。 -文档读取顺序: +文档读取规则: -- 后端 Java、Mapper、Service、Controller、BO、VO、Entity、权限、查询、公共模块或 JavaDoc 任务,先读 `.codex/skills/ruoyi-plus-ai-coding/references/backend.md`。 -- 同步前端 Vue、React、TypeScript、api、types 或页面骨架时,再读 `.codex/skills/ruoyi-plus-ai-coding/references/frontend.md`。 -- 任务边界不清晰或需要标准场景示例时,再读 `.codex/skills/ruoyi-plus-ai-coding/references/examples.md`。 -- 只读取当前任务相关的 reference,不一次性展开全部文档。 -- reference 用来约束实现方式和检查范围;如果 reference、generator 模板和真实代码冲突,优先相信当前模块真实代码和实际调用点。 +- 按任务类型读取上面对应的 `.claude/agents` 文档,只读取当前任务相关内容。 +- 同步前端时,直接阅读目标 Vue 或 React 工程的现有 API、types、页面及对应 generator 模板,确认路由、返回结构和日期范围参数。 +- 需要场景示例时,优先阅读当前模块最近似实现及实际调用点。 +- 本套规则独立维护,不依赖 `.codex` 下的技能或 reference;规则、generator 模板和真实代码冲突时,优先相信当前模块真实代码和实际调用点。 通用要求: - 先读同模块最近似实现,再动代码。 +- 新增工具类或通用辅助方法前,必须执行下方“已有能力复用”流程。 - 发生冲突时优先相信当前模块真实代码,其次是公共基础设施,再其次才是 generator 模板。 - 默认直接产出可落地代码,而不是只给抽象建议。 - 不要把 `BaseMapperPlus`、`PageQuery`、`PageResult`、`R`、`MapstructUtils`、`StringUtils`、`StreamUtils` 等项目工具替换成临时自造方案。 - import、注解顺序、文件结构以附近代码为准,不做无关重排。 - 修改公开 API 前先查调用点;公共模块优先保持方法签名、泛型、返回值和异常语义兼容。 + +## 已有能力复用 + +1. 按能力类别检索同模块、`ruoyi-common` 和已有依赖,例如类型转换、字符串处理、集合处理、日期计算,不只搜索拟新增的方法名。 +2. 优先阅读实际调用示例,再核对候选实现或依赖 API;结合目标模块的 `pom.xml`、传递依赖及版本确认可用性,不能仅凭其他模块有调用或根 POM 管理了版本就认定可用。 +3. 核对空值、非法输入、默认值、异常和数值精度(含溢出、截断),依赖行为以当前版本的实现或测试为准。满足业务语义时直接复用,不添加无业务语义的包装;不满足时保留必要的业务差异,不能为了复用改变业务行为。 + +例如容错 ID 转换可先查已有调用中的 Hutool `Convert.toLong`,确认所用重载的失败返回值和默认值符合需求;必须抛错的严格解析不能改成返回 `null` 或默认值,还要核对小数、越界等输入的处理。这里只示范核对方式,不维护全量工具 API 清单。 + +交付自检: + +- 新增工具类或通用辅助方法是否已检索并核对同类能力,避免重复实现。 +- 确需新增时,在交付说明中简述现有能力为何不适用,无需为此额外添加代码注释;既有 JavaDoc 规则仍适用。 diff --git a/.claude/agents/backend-module-enhancement.md b/.claude/agents/backend-module-enhancement.md index fe25515e2..8164522a9 100644 --- a/.claude/agents/backend-module-enhancement.md +++ b/.claude/agents/backend-module-enhancement.md @@ -12,6 +12,7 @@ description: 复杂后端模块增强专家。用于修改当前项目中已经 3. 保留已有的数据权限、事务、缓存、导入导出、唯一性校验、删除前校验。 4. 不能为了“简洁”把复杂模块退化成模板式单表 CRUD。 5. 修改 workflow 模块时检查同包是否需要 `@ConditionalOnEnable`。 +6. 新增工具类或通用辅助方法前,先读取 `.claude/agents/backend-engineering.md` 的“已有能力复用”规则,按能力类别检索、确认模块依赖可用性并核对业务语义;满足需求时直接复用,不添加无业务语义的包装。 ## 常见任务 @@ -33,6 +34,7 @@ description: 复杂后端模块增强专家。用于修改当前项目中已经 ## 自检 +- 新增工具类或通用辅助方法是否已完成同类能力检索与语义核对;确需新增时,在交付说明中简述现有能力为何不适用,无需为此额外添加代码注释,既有 JavaDoc 规则仍适用 - 是否破坏了原模块的权限边界 - 是否误删了旧逻辑中的事务或校验 - 是否错误简化了复杂关系维护 diff --git a/.claude/agents/backend-query-permission.md b/.claude/agents/backend-query-permission.md index adcc168d8..be8bd417f 100644 --- a/.claude/agents/backend-query-permission.md +++ b/.claude/agents/backend-query-permission.md @@ -12,6 +12,7 @@ description: 后端查询、联表与数据权限专家。用于当前项目中 3. 复杂联表优先参考 MPJ 风格,不轻易改回手写零散 SQL。 4. 如果 `BaseMapperPlus + wrapper` 足够,不要额外补 XML。 5. wrapper 查询条件优先使用项目已有工具和命名,不自造查询 DSL。 +6. 新增工具类或通用辅助方法前,先读取 `.claude/agents/backend-engineering.md` 的“已有能力复用”规则,按能力类别检索、确认模块依赖可用性并核对业务语义;满足需求时直接复用,不添加无业务语义的包装。 ## 重点关注 @@ -38,6 +39,7 @@ description: 后端查询、联表与数据权限专家。用于当前项目中 ## 输出要求 +- 自检新增工具类或通用辅助方法是否已完成同类能力检索与语义核对;确需新增时,在交付说明中简述现有能力为何不适用,无需为此额外添加代码注释,既有 JavaDoc 规则仍适用 - 明确说明查询是单表、联表还是带权限控制 - 保持与当前模块 mapper 风格一致 - 不要让查询参数风格和前端现有调用脱节 diff --git a/.codex/skills/ruoyi-plus-ai-coding/SKILL.md b/.codex/skills/ruoyi-plus-ai-coding/SKILL.md index 488da20ee..1608231d5 100644 --- a/.codex/skills/ruoyi-plus-ai-coding/SKILL.md +++ b/.codex/skills/ruoyi-plus-ai-coding/SKILL.md @@ -31,7 +31,7 @@ description: 在仓库内按代码生成器模板、项目 reference 文档和 ## 执行流程 1. 先判断任务类型,并按“文档读取规则”读取当前任务需要的 reference。 -2. 确认目标模块,优先复用同模块中最近似功能的写法。 +2. 确认目标模块,优先复用同模块中最近似功能的写法;新增工具类或通用辅助方法前,执行“已有能力检索”。 3. 新增标准 CRUD 代码前,先读取 `ruoyi-modules/ruoyi-gen/src/main/resources/fm/` 下的 FreeMarker 模板。 4. 命名和分层保持与仓库一致: `domain` entity、`domain.bo`、`domain.vo`、`mapper`、`service`、`service.impl`、`controller`。 @@ -40,6 +40,12 @@ description: 在仓库内按代码生成器模板、项目 reference 文档和 7. 修改 `ruoyi-common` 公共模块前,先阅读同包接口、实现类和调用点,优先保持已有 API 语义与兼容性。 8. 只补注释或文档时,不运行无关格式化,不重排 import,不改代码逻辑。 +### 已有能力检索 + +1. 按能力类别搜索同模块、`ruoyi-common` 和已有依赖,例如类型转换、字符串处理、集合处理、日期计算;不要只搜拟新增的方法名。 +2. 优先阅读实际调用示例,再核对候选实现或依赖 API;结合目标模块的 `pom.xml`、传递依赖及版本确认可用性,不能仅凭仓库其他模块有调用或根 POM 管理了版本就认定可用。 +3. 按适用 reference 核对业务语义;满足需求时直接复用,不添加无业务语义的包装。不满足时保留必要的业务差异,再决定是否新增。 + ## 文档读取规则 使用本 skill 时,先按任务类型读取适用 reference,不一次性展开所有文档: @@ -148,9 +154,11 @@ Vue 3、React、TypeScript API 文件、生成式列表页、表单状态、字 - 查询、分页、删除校验、写入校验逻辑闭环完整。 - 权限、日志、防重、事务、数据权限尽量贴近同模块现有实现。 - 如果同步改前端,前端 API 路径和后端接口保持一致。 +- 确需新增工具类或通用辅助方法时,在交付说明中简述检索过的现有能力为何不适用,无需为此额外添加代码注释;既有 JavaDoc 规则仍适用。 ## 快速检查清单 +- 新增工具类或通用辅助方法是否已完成同类能力检索与语义核对,避免重复实现或无业务语义的包装。 - 包路径和 `@RequestMapping` 与模块保持一致。 - 权限标识遵循 `${module}:${business}:${action}`。 - Mapper 继承 `BaseMapperPlus`。 diff --git a/.codex/skills/ruoyi-plus-ai-coding/references/backend.md b/.codex/skills/ruoyi-plus-ai-coding/references/backend.md index cc9841fac..fb1c3a06a 100644 --- a/.codex/skills/ruoyi-plus-ai-coding/references/backend.md +++ b/.codex/skills/ruoyi-plus-ai-coding/references/backend.md @@ -166,6 +166,8 @@ - 分页统一使用 `PageQuery` 和 `PageResult`,不要无故引入新的分页 DTO。 - 优先使用项目工具类:`MapstructUtils`、`StringUtils`、`StreamUtils`、`ValidatorUtils`、`SpringUtils`、`RedisUtils`。 +- 复用前核对空值、非法输入、默认值、异常和数值精度(含溢出、截断)的行为,不能为了复用改变业务语义;依赖行为以当前版本的实现或测试为准。 +- 例如容错 ID 转换可先查已有调用中的 Hutool `Convert.toLong`,核对所用重载的失败返回值与默认值是否符合需求;必须抛错的严格解析不能改成返回 `null` 或默认值,也要核对小数、越界等输入的处理。此处只示范核对方式,不维护全量工具 API 清单。 - 数组转列表按附近代码习惯使用 `List.of(ids)` 或 `Arrays.asList(ids)`。 - 日期范围查询通常从 `bo.getParams()` 中读取 `beginTime`、`endTime` 或 `beginFieldName`、`endFieldName`。 - 构建查询优先识别 `QueryBuilder.lambda(...)`、`QueryBuilder.lambdaJoin(...)`、`BaseMapperPlus#lambda()` 三类入口,不要退回临时手写 SQL 或自造 wrapper。 diff --git a/.run/ruoyi-monitor-admin.run.xml b/.run/ruoyi-monitor-admin.run.xml index d12ffd796..f850f70ac 100644 --- a/.run/ruoyi-monitor-admin.run.xml +++ b/.run/ruoyi-monitor-admin.run.xml @@ -2,7 +2,7 @@ - diff --git a/.run/ruoyi-server.run.xml b/.run/ruoyi-server.run.xml index 29edbe4f3..317908ab7 100644 --- a/.run/ruoyi-server.run.xml +++ b/.run/ruoyi-server.run.xml @@ -2,7 +2,7 @@ - diff --git a/.run/ruoyi-snailai-server.run.xml b/.run/ruoyi-snailai-server.run.xml index 0939d69f7..a672a37e8 100644 --- a/.run/ruoyi-snailai-server.run.xml +++ b/.run/ruoyi-snailai-server.run.xml @@ -3,7 +3,7 @@ server-name="Docker"> - diff --git a/.run/ruoyi-snailjob-server.run.xml b/.run/ruoyi-snailjob-server.run.xml index a28e62df2..13024cedd 100644 --- a/.run/ruoyi-snailjob-server.run.xml +++ b/.run/ruoyi-snailjob-server.run.xml @@ -2,7 +2,7 @@ - diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..6bb9a8122 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,6 @@ +# 项目指令 + +## 已有能力复用 + +- 新增工具类或通用辅助方法前,必须检索同模块、`ruoyi-common` 和已有依赖中的同类能力;满足业务语义且当前模块可用时直接复用,不添加无业务语义的包装。 +- 检索流程、后端语义核对与交付自检见 `.codex/skills/ruoyi-plus-ai-coding/SKILL.md` 及其适用 reference。 diff --git a/README.md b/README.md index 003a39045..0128effae 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![Star](https://gitcode.com/dromara/RuoYi-Vue-Plus/star/badge.svg)](https://gitcode.com/dromara/RuoYi-Vue-Plus) [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/6.X/LICENSE)
-[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-6.0.0-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus) +[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-6.1.0-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus) [![Spring Boot](https://img.shields.io/badge/Spring%20Boot-4.1-blue.svg)]() [![JDK-21](https://img.shields.io/badge/JDK-21-green.svg)]() [![JDK-25](https://img.shields.io/badge/JDK-25-green.svg)]() diff --git a/pom.xml b/pom.xml index 0d18cf3df..0d3cf8d04 100644 --- a/pom.xml +++ b/pom.xml @@ -14,23 +14,23 @@ - 6.0.0 + 6.1.0 UTF-8 UTF-8 21 - 4.1.1 - 4.1.2 - 2.0.1 + 4.1.2 + 4.1.4 + 2.0.2 3.1.1 3.5.19 - 3.5.17 + 3.5.18 1.5.9 - 4.5.0 - 8.7.3-20260306 + 4.6.0 + 17.7.6-20261020 3.0.2 7.17.28 @@ -40,7 +40,7 @@ 1.86 - 4.7.0 + 4.8.0 2.2.7 2.0.2 1.1.1 @@ -51,8 +51,8 @@ 0.2.0 1.18.48 0.15.0 - 2.0.2-incubating - 1.7.4 + 2.1.0-incubating + 1.7.7 @@ -62,8 +62,8 @@ 3.3.5 - 1.8.9 - 2.16.2.1 + 2.1.1 + 2.16.3.1 2.6.12 @@ -357,13 +357,13 @@ - org.dromara.warm + io.github.sgs98 warm-flow-mybatis-plus-sb4-starter ${warm-flow.version} - org.dromara.warm + io.github.sgs98 warm-flow-plugin-ui-sb-web ${warm-flow.version} diff --git a/ruoyi-admin/Dockerfile b/ruoyi-admin/Dockerfile index 8923d2a67..2dad93a30 100644 --- a/ruoyi-admin/Dockerfile +++ b/ruoyi-admin/Dockerfile @@ -1,6 +1,6 @@ # 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/ -FROM bellsoft/liberica-openjdk-rocky:21.0.12-cds -# FROM bellsoft/liberica-openjdk-rocky:25.0.4-cds +FROM bellsoft/liberica-openjdk-rocky:21.0.13-cds +# FROM bellsoft/liberica-openjdk-rocky:25.0.5-cds # FROM findepi/graalvm:java21-native LABEL maintainer="Lion Li" diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index ae9b6ff9e..9b5dd8628 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -82,6 +82,8 @@ mybatis-plus: enabled: true # 输出方式,可选 console、log output: console + # 忽略高频轮询表的成功 SQL 日志;执行异常仍会输出 + ignore-tables: [flow_event_wait] --- # 数据源配置 spring: diff --git a/ruoyi-admin/src/main/resources/application-prod.yml b/ruoyi-admin/src/main/resources/application-prod.yml index e581b182c..41c587d68 100644 --- a/ruoyi-admin/src/main/resources/application-prod.yml +++ b/ruoyi-admin/src/main/resources/application-prod.yml @@ -85,6 +85,8 @@ mybatis-plus: enabled: false # 输出方式,可选 console、log output: console + # 忽略高频轮询表的成功 SQL 日志;执行异常仍会输出 + ignore-tables: [ flow_event_wait ] --- # 数据源配置 spring: diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 78efbb4f6..f6a6957ab 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -31,6 +31,8 @@ logging: org.springframework: warn org.mybatis.spring.mapper: error org.apache.fury: warn + com.yomahub.liteflow: warn + org.anyline: warn config: classpath:logback-plus.xml # 用户配置 @@ -239,19 +241,22 @@ warm-flow: ui: true # 是否显示流程图顶部文字 top-text-show: true + # 流程图状态颜色:未办理、待办理、已办理(RGB) + chart-status-color: ["107,114,128", "245,158,11", "64,158,255"] # 是否渲染节点悬浮提示,默认true node-tooltip: true # 默认Authorization,如果有多个token,用逗号分隔 token-name: ${sa-token.token-name},clientid + # 是否开启定时/事件引擎线程(默认false)。开启后常驻扫描线程池, + # 已发布的「定时启动」流程定义会按配置自动发起实例 + timer-enabled: true --- # LiteFlow 业务规则编排 liteflow: # 跟随工作流模块开关,避免禁用工作流时仍加载任务办理链 enable: ${warm-flow.enabled:true} metrics: - # 临时修复:LiteFlow 2.16.2.1 的 Actuator 端点与 Spring Boot 4.1 参数名校验不兼容 - # 待 LiteFlow 修复该问题后恢复为 ${liteflow.enable:true} - enabled: false + enabled: ${liteflow.enable:true} # 工作流模块用于编排任务办理链路 rule-source: classpath:liteflow/*.el.xml # 开启虚拟线程 diff --git a/ruoyi-admin/src/main/resources/logback-plus.xml b/ruoyi-admin/src/main/resources/logback-plus.xml index 2d786f9a9..c48e2e7b0 100644 --- a/ruoyi-admin/src/main/resources/logback-plus.xml +++ b/ruoyi-admin/src/main/resources/logback-plus.xml @@ -117,6 +117,8 @@ + + diff --git a/ruoyi-api/src/main/java/org/dromara/system/api/UserService.java b/ruoyi-api/src/main/java/org/dromara/system/api/UserService.java index 09824133b..d1b8009ef 100644 --- a/ruoyi-api/src/main/java/org/dromara/system/api/UserService.java +++ b/ruoyi-api/src/main/java/org/dromara/system/api/UserService.java @@ -93,6 +93,14 @@ public interface UserService { */ List selectUsersByDeptIds(Collection deptIds); + /** + * 通过部门ID查询用户ID + * + * @param deptIds 部门ids + * @return 用户ids + */ + List selectUserIdsByDeptIds(Collection deptIds); + /** * 通过岗位ID查询用户 * @@ -101,6 +109,14 @@ public interface UserService { */ List selectUsersByPostIds(Collection postIds); + /** + * 通过岗位ID查询用户ID + * + * @param postIds 岗位ids + * @return 用户ids + */ + List selectUserIdsByPostIds(Collection postIds); + /** * 根据用户 ID 列表查询用户昵称映射关系 * diff --git a/ruoyi-api/src/main/java/org/dromara/system/api/model/SmsLoginBody.java b/ruoyi-api/src/main/java/org/dromara/system/api/model/SmsLoginBody.java index 9ffa3de6b..b88d1c3e7 100644 --- a/ruoyi-api/src/main/java/org/dromara/system/api/model/SmsLoginBody.java +++ b/ruoyi-api/src/main/java/org/dromara/system/api/model/SmsLoginBody.java @@ -1,8 +1,10 @@ package org.dromara.system.api.model; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; import lombok.Data; import lombok.EqualsAndHashCode; +import org.dromara.common.core.constant.RegexConstants; import org.dromara.common.core.domain.model.LoginBody; /** @@ -18,6 +20,7 @@ public class SmsLoginBody extends LoginBody { * 手机号 */ @NotBlank(message = "{user.phonenumber.not.blank}") + @Pattern(regexp = RegexConstants.MOBILE, message = "手机号格式不正确") private String phoneNumber; /** diff --git a/ruoyi-api/src/main/java/org/dromara/workflow/api/WorkflowService.java b/ruoyi-api/src/main/java/org/dromara/workflow/api/WorkflowService.java index 16ea0551e..5053ac7ff 100644 --- a/ruoyi-api/src/main/java/org/dromara/workflow/api/WorkflowService.java +++ b/ruoyi-api/src/main/java/org/dromara/workflow/api/WorkflowService.java @@ -3,6 +3,9 @@ package org.dromara.workflow.api; import org.dromara.workflow.api.domain.CompleteTaskDTO; import org.dromara.workflow.api.domain.StartProcessDTO; import org.dromara.workflow.api.domain.StartProcessReturnDTO; +import org.dromara.workflow.api.domain.StartMessageEventDTO; +import org.dromara.workflow.api.domain.StartTimerEventDTO; +import org.dromara.workflow.api.domain.TriggerMessageEventDTO; import java.util.List; import java.util.Map; @@ -96,4 +99,48 @@ public interface WorkflowService { * @return 首节点办理成功返回 {@code true} */ boolean startCompleteTask(StartProcessDTO startProcess); + + /** + * 外部消息按流程编码和事件名称启动新实例。 + * + * @param request 消息启动参数,业务 ID 由调用方提供 + * @return 新建流程实例 ID + */ + Long startByMessageEvent(StartMessageEventDTO request); + + /** + * 触发实例中等待的消息捕获或消息边界事件。重复投递时引擎保持实例不变。 + * + * @param request 消息事件参数 + * @return 流程实例 ID + */ + Long triggerMessageEvent(TriggerMessageEventDTO request); + + /** + * 手动发起定时启动事件,不改变等待记录及下一次自动触发时间。 + * + * @param request 定时启动参数,业务 ID 留空时生成数字雪花 ID + * @return 新建流程实例 ID + */ + Long startByTimerEvent(StartTimerEventDTO request); + + /** + * 从实例级错误捕获节点处理外部错误。 + * + * @param instanceId 流程实例 ID + * @param errorCode 错误码,精确匹配优先于通配 + * @param errorMessage 错误描述 + * @return 错误事件已被处理时返回 true + */ + boolean throwInstanceErrorEvent(Long instanceId, String errorCode, String errorMessage); + + /** + * 从当前待办的错误边界节点处理外部错误。 + * + * @param taskId 当前待办任务 ID + * @param errorCode 错误码,精确匹配优先于通配 + * @param errorMessage 错误描述 + * @return 错误事件已被处理时返回 true + */ + boolean throwTaskErrorEvent(Long taskId, String errorCode, String errorMessage); } diff --git a/ruoyi-api/src/main/java/org/dromara/workflow/api/domain/StartMessageEventDTO.java b/ruoyi-api/src/main/java/org/dromara/workflow/api/domain/StartMessageEventDTO.java new file mode 100644 index 000000000..3de84cea6 --- /dev/null +++ b/ruoyi-api/src/main/java/org/dromara/workflow/api/domain/StartMessageEventDTO.java @@ -0,0 +1,42 @@ +package org.dromara.workflow.api.domain; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Map; + +/** + * 消息启动事件请求。 + */ +@Data +public class StartMessageEventDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 业务主键,调用方必须提供。 + */ + private String businessId; + + /** + * 流程定义编码。 + */ + private String flowCode; + + /** + * 消息启动事件名称。 + */ + private String eventName; + + /** + * 合并到流程变量的消息数据,可为空。 + */ + private Map payload; + + /** + * 操作人,留空时使用系统处理人。 + */ + private String handler; +} diff --git a/ruoyi-api/src/main/java/org/dromara/workflow/api/domain/StartTimerEventDTO.java b/ruoyi-api/src/main/java/org/dromara/workflow/api/domain/StartTimerEventDTO.java new file mode 100644 index 000000000..56be733b3 --- /dev/null +++ b/ruoyi-api/src/main/java/org/dromara/workflow/api/domain/StartTimerEventDTO.java @@ -0,0 +1,42 @@ +package org.dromara.workflow.api.domain; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Map; + +/** + * 手动发起定时启动事件请求。 + */ +@Data +public class StartTimerEventDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 业务主键,留空时生成数字雪花 ID。 + */ + private String businessId; + + /** + * 已发布的流程定义 ID。 + */ + private Long definitionId; + + /** + * 定时启动事件节点编码。 + */ + private String nodeCode; + + /** + * 合并到流程变量的业务数据,可为空。 + */ + private Map payload; + + /** + * 操作人,留空时使用系统处理人。 + */ + private String handler; +} diff --git a/ruoyi-api/src/main/java/org/dromara/workflow/api/domain/TriggerMessageEventDTO.java b/ruoyi-api/src/main/java/org/dromara/workflow/api/domain/TriggerMessageEventDTO.java new file mode 100644 index 000000000..ea354fad1 --- /dev/null +++ b/ruoyi-api/src/main/java/org/dromara/workflow/api/domain/TriggerMessageEventDTO.java @@ -0,0 +1,37 @@ +package org.dromara.workflow.api.domain; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Map; + +/** + * 触发实例中等待的消息事件请求。 + */ +@Data +public class TriggerMessageEventDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 已存在的流程实例 ID。 + */ + private Long instanceId; + + /** + * 等待中的消息捕获或消息边界事件名称。 + */ + private String eventName; + + /** + * 合并到流程变量的消息数据,可为空。 + */ + private Map payload; + + /** + * 操作人,留空时使用系统处理人。 + */ + private String handler; +} diff --git a/ruoyi-common/ruoyi-common-bom/pom.xml b/ruoyi-common/ruoyi-common-bom/pom.xml index 8d8b12dca..b0962ad95 100644 --- a/ruoyi-common/ruoyi-common-bom/pom.xml +++ b/ruoyi-common/ruoyi-common-bom/pom.xml @@ -14,7 +14,7 @@ - 6.0.0 + 6.1.0 diff --git a/ruoyi-common/ruoyi-common-mybatis/pom.xml b/ruoyi-common/ruoyi-common-mybatis/pom.xml index af3e623f1..1dee1e949 100644 --- a/ruoyi-common/ruoyi-common-mybatis/pom.xml +++ b/ruoyi-common/ruoyi-common-mybatis/pom.xml @@ -40,6 +40,12 @@ dynamic-datasource-spring-boot4-starter + + org.springframework.boot + spring-boot-actuator + provided + + com.baomidou @@ -58,6 +64,12 @@ mybatis-plus-join-boot-starter + + com.h2database + h2 + test + + diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/com/baomidou/dynamic/datasource/spring/boot/autoconfigure/DynamicDataSourceEndpoint.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/com/baomidou/dynamic/datasource/spring/boot/autoconfigure/DynamicDataSourceEndpoint.java new file mode 100644 index 000000000..8d8ab615a --- /dev/null +++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/com/baomidou/dynamic/datasource/spring/boot/autoconfigure/DynamicDataSourceEndpoint.java @@ -0,0 +1,88 @@ +/* + * Copyright © 2018 organization baomidou + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.baomidou.dynamic.datasource.spring.boot.autoconfigure; + +import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; +import com.baomidou.dynamic.datasource.ds.GroupDataSource; +import com.baomidou.dynamic.datasource.ds.ItemDataSource; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; + +import javax.sql.DataSource; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 只读运维端点:暴露当前所有数据源(含运行期动态增删的)、分组构成与全局开关状态, + * 访问路径 {@code /actuator/dynamicDatasource}(需按 actuator 惯例显式暴露)。 + *

+ * 刻意不返回 URL / 用户名等连接串信息——URL 可能携带凭据,状态盘点不需要它们。 + */ +@Endpoint(id = "dynamicDatasource") +public class DynamicDataSourceEndpoint { + + private final ObjectProvider dataSources; + private final DynamicDataSourceProperties properties; + + public DynamicDataSourceEndpoint(ObjectProvider dataSources, DynamicDataSourceProperties properties) { + this.dataSources = dataSources; + this.properties = properties; + } + + @ReadOperation + public Map summary() { + Map result = new LinkedHashMap<>(); + result.put("primary", properties.getPrimary()); + result.put("strict", properties.getStrict()); + result.put("p6spy", properties.getP6spy()); + result.put("seata", properties.getSeata()); + + DataSource dataSource = dataSources.getIfAvailable(); + if (!(dataSource instanceof DynamicRoutingDataSource)) { + result.put("dataSources", Collections.emptyMap()); + result.put("groups", Collections.emptyMap()); + return result; + } + DynamicRoutingDataSource routingDataSource = (DynamicRoutingDataSource) dataSource; + + Map dataSourceSummary = new LinkedHashMap<>(); + for (Map.Entry entry : routingDataSource.getDataSources().entrySet()) { + dataSourceSummary.put(entry.getKey(), poolType(entry.getValue())); + } + result.put("dataSources", dataSourceSummary); + + Map groupSummary = new LinkedHashMap<>(); + for (Map.Entry entry : routingDataSource.getGroupDataSources().entrySet()) { + Map group = new LinkedHashMap<>(); + group.put("size", entry.getValue().size()); + group.put("dataSources", new ArrayList<>(entry.getValue().getDataSourceMap().keySet())); + groupSummary.put(entry.getKey(), group); + } + result.put("groups", groupSummary); + return result; + } + + private String poolType(DataSource dataSource) { + DataSource target = dataSource; + if (dataSource instanceof ItemDataSource) { + target = ((ItemDataSource) dataSource).getRealDataSource(); + } + return target.getClass().getSimpleName(); + } +} diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/config/properties/SqlLogProperties.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/config/properties/SqlLogProperties.java index fe07f0105..a30fc5b07 100644 --- a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/config/properties/SqlLogProperties.java +++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/config/properties/SqlLogProperties.java @@ -3,6 +3,9 @@ package org.dromara.common.mybatis.config.properties; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; +import java.util.ArrayList; +import java.util.List; + /** * SQL 日志配置。 * @@ -22,4 +25,9 @@ public class SqlLogProperties { */ private String output = "console"; + /** + * 不输出成功执行日志的表名;执行异常仍会输出。 + */ + private List ignoreTables = new ArrayList<>(); + } diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/interceptor/SqlLogInterceptor.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/interceptor/SqlLogInterceptor.java index d17ab7810..388b8a39b 100644 --- a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/interceptor/SqlLogInterceptor.java +++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/interceptor/SqlLogInterceptor.java @@ -30,6 +30,7 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.UUID; +import java.util.regex.Pattern; /** * 完整 SQL 日志拦截器。 @@ -55,6 +56,8 @@ public class SqlLogInterceptor implements Interceptor { */ private static final String BLANK_REGEX = "\\s+"; + private static final Pattern SQL_STRING_LITERAL = Pattern.compile("'(?:''|[^'])*'"); + /** * 日期时间格式。 */ @@ -75,8 +78,17 @@ public class SqlLogInterceptor implements Interceptor { */ private final SqlLogProperties sqlLogProperties; + private final List ignoredTablePatterns; + public SqlLogInterceptor(SqlLogProperties sqlLogProperties) { this.sqlLogProperties = sqlLogProperties; + this.ignoredTablePatterns = sqlLogProperties.getIgnoreTables().stream() + .filter(StrUtil::isNotBlank) + .map(String::trim) + .map(table -> Pattern.compile("(?i)\\b(?:FROM|JOIN|UPDATE|INTO)\\s+" + + "(?:[`\"\\[]?[A-Za-z_][A-Za-z0-9_]*[`\"\\]]?\\s*\\.\\s*)?" + + "[`\"\\[]?" + Pattern.quote(table) + "[`\"\\]]?(?=\\s|[,);]|$)")) + .toList(); } @Override @@ -104,11 +116,27 @@ public class SqlLogInterceptor implements Interceptor { * @param throwable 执行异常 */ private void printSql(MappedStatement mappedStatement, BoundSql boundSql, long elapsedTime, Throwable throwable) { + if (throwable == null && isIgnoredTableSql(boundSql.getSql())) { + return; + } String fullSql = buildFullSql(mappedStatement.getConfiguration(), boundSql); String message = buildLogMessage(mappedStatement, elapsedTime, fullSql, throwable); printChunk(message); } + boolean isIgnoredTableSql(String sql) { + if (ignoredTablePatterns.isEmpty()) { + return false; + } + String statement = sql.indexOf('\'') >= 0 ? SQL_STRING_LITERAL.matcher(sql).replaceAll("''") : sql; + for (Pattern pattern : ignoredTablePatterns) { + if (pattern.matcher(statement).find()) { + return true; + } + } + return false; + } + /** * 构建日志内容。 * diff --git a/ruoyi-common/ruoyi-common-mybatis/src/test/java/org/dromara/common/mybatis/core/mapper/MybatisPlusH2IntegrationTest.java b/ruoyi-common/ruoyi-common-mybatis/src/test/java/org/dromara/common/mybatis/core/mapper/MybatisPlusH2IntegrationTest.java new file mode 100644 index 000000000..f5950758f --- /dev/null +++ b/ruoyi-common/ruoyi-common-mybatis/src/test/java/org/dromara/common/mybatis/core/mapper/MybatisPlusH2IntegrationTest.java @@ -0,0 +1,254 @@ +package org.dromara.common.mybatis.core.mapper; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.MybatisSqlSessionFactoryBuilder; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.core.toolkit.GlobalConfigUtils; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler; +import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.github.yulichang.base.MPJBaseMapper; +import com.github.yulichang.injector.MPJSqlInjector; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import org.apache.ibatis.exceptions.TooManyResultsException; +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.mybatis.core.query.QueryBuilder; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; +import org.apache.ibatis.datasource.unpooled.UnpooledDataSource; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("MyBatis-Plus H2 SQL 集成测试") +class MybatisPlusH2IntegrationTest { + + private SqlSessionFactory sqlSessionFactory; + + @BeforeEach + void setUp() throws Exception { + String url = "jdbc:h2:mem:mp_" + UUID.randomUUID().toString().replace("-", "") + ";DB_CLOSE_DELAY=-1"; + UnpooledDataSource dataSource = new UnpooledDataSource("org.h2.Driver", url, "sa", ""); + try (Connection connection = dataSource.getConnection(); Statement statement = connection.createStatement()) { + statement.execute("CREATE TABLE audit_record (id BIGINT PRIMARY KEY, owner_id BIGINT, name VARCHAR(40))"); + statement.execute("CREATE TABLE audit_extra (record_id BIGINT PRIMARY KEY, flag INT)"); + statement.execute("INSERT INTO audit_record VALUES (1, 1, 'one'), (2, 2, 'two'), (3, 1, 'three'), (4, 2, 'four')"); + statement.execute("INSERT INTO audit_extra VALUES (1, 0), (2, 1), (3, 0), (4, 0)"); + } + + MybatisConfiguration configuration = new MybatisConfiguration(); + configuration.setEnvironment(new Environment("h2", new JdbcTransactionFactory(), dataSource)); + configuration.setMapUnderscoreToCamelCase(true); + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MultiDataPermissionHandler() { + @Override + public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) { + if (!"audit_record".equalsIgnoreCase(table.getName())) { + return null; + } + String qualifier = table.getAlias() == null ? "audit_record" : table.getAlias().getName(); + try { + return CCJSqlParserUtil.parseCondExpression(qualifier + ".owner_id = 1"); + } catch (JSQLParserException e) { + throw new IllegalStateException(e); + } + } + })); + interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); + configuration.addInterceptor(interceptor); + GlobalConfigUtils.setGlobalConfig(configuration, new GlobalConfig() + .setDbConfig(new GlobalConfig.DbConfig()) + .setSqlInjector(new MPJSqlInjector())); + configuration.addMapper(AuditMapper.class); + configuration.addMapper(AuditExtraMapper.class); + sqlSessionFactory = new MybatisSqlSessionFactoryBuilder().build(configuration); + } + + @Test + @DisplayName("联表分页保留权限过滤和总数") + void shouldPageJoinedQueryWithPermission() { + try (SqlSession session = sqlSessionFactory.openSession()) { + Page page = new Page<>(1, 1); + List records = session.getMapper(AuditMapper.class).findPage(page, 1L); + + assertEquals(2, page.getTotal()); + assertEquals(List.of(1L), records.stream().map(AuditRecord::getId).toList()); + } + } + + @Test + @DisplayName("OR 查询和更新均受权限条件约束") + void shouldRestrictOrSelectAndUpdate() { + try (SqlSession session = sqlSessionFactory.openSession()) { + AuditMapper mapper = session.getMapper(AuditMapper.class); + + assertEquals(List.of(1L), mapper.findByEitherId(1L, 2L).stream().map(AuditRecord::getId).toList()); + assertEquals(1, mapper.renameEitherId(1L, 2L)); + assertEquals("changed", mapper.selectById(1L).getName()); + session.commit(); + try (Connection connection = sqlSessionFactory.getConfiguration().getEnvironment().getDataSource().getConnection(); + Statement statement = connection.createStatement(); + ResultSet result = statement.executeQuery("SELECT name FROM audit_record WHERE id = 2")) { + assertTrue(result.next()); + assertEquals("two", result.getString(1)); + } catch (Exception e) { + fail(e); + } + } + } + + @Test + @DisplayName("selectOne 多行时保持异常和非异常模式") + void shouldKeepSelectOneSemantics() { + try (SqlSession session = sqlSessionFactory.openSession()) { + AuditMapper mapper = session.getMapper(AuditMapper.class); + LambdaQueryWrapper query = new LambdaQueryWrapper() + .eq(AuditRecord::getOwnerId, 1L); + + assertThrows(TooManyResultsException.class, () -> mapper.selectOne(query)); + assertEquals(1L, mapper.selectOne(query, false).getId()); + } + } + + @Test + @DisplayName("BaseMapperPlus 链式查询使用真实 Mapper SQL") + void shouldExecuteMapperChain() { + try (SqlSession session = sqlSessionFactory.openSession()) { + AuditMapper mapper = session.getMapper(AuditMapper.class); + + assertEquals(List.of(1L, 3L), mapper.lambda() + .eq(AuditRecord::getOwnerId, 1L) + .orderByAsc(AuditRecord::getId) + .list().stream().map(AuditRecord::getId).toList()); + } + } + + @Test + @DisplayName("PageQuery 分页正确返回记录和总数") + void shouldExecutePageQuery() { + try (SqlSession session = sqlSessionFactory.openSession()) { + PageQuery pageQuery = new PageQuery(1, 2); + Page page = pageQuery.build(); + List records = session.getMapper(AuditMapper.class).selectList(page, + QueryBuilder.lambda(AuditRecord.class) + .eq(AuditRecord::getOwnerId, 1L) + .orderByAsc(AuditRecord::getId) + .build()); + + assertEquals(2, page.getTotal()); + assertEquals(List.of(3L), records.stream().map(AuditRecord::getId).toList()); + } + } + + @Test + @DisplayName("BaseMapperPlus 基础写入和删除实际执行") + void shouldExecuteCrud() { + try (SqlSession session = sqlSessionFactory.openSession()) { + AuditMapper mapper = session.getMapper(AuditMapper.class); + AuditRecord record = new AuditRecord(); + record.id = 5L; + record.ownerId = 1L; + record.name = "five"; + + assertEquals(1, mapper.insert(record)); + assertEquals("five", mapper.selectById(5L).getName()); + record.name = "updated"; + assertEquals(1, mapper.updateById(record)); + assertEquals("updated", mapper.selectById(5L).getName()); + assertEquals(1, mapper.deleteById(5L)); + assertNull(mapper.selectById(5L)); + } + } + + @Test + @DisplayName("MPJ 联表分页正确过滤记录和统计总数") + void shouldExecuteMpjJoinPage() { + try (SqlSession session = sqlSessionFactory.openSession()) { + AuditMapper mapper = session.getMapper(AuditMapper.class); + Page page = mapper.selectJoinPage(new Page<>(1, 1), AuditRecord.class, + QueryBuilder.lambdaJoin("r", AuditRecord.class) + .selectAll(AuditRecord.class) + .leftJoin(AuditExtra.class, "e", AuditExtra::getRecordId, AuditRecord::getId) + .eq("e", AuditExtra::getFlag, 0) + .build()); + + assertEquals(2, page.getTotal()); + assertEquals(1, page.getRecords().size()); + assertEquals(1L, page.getRecords().getFirst().getId()); + } + } + + public interface AuditMapper extends BaseMapperPlus, MPJBaseMapper { + + @Select("SELECT r.id, r.owner_id, r.name FROM audit_record r LEFT JOIN audit_extra e ON e.record_id = r.id " + + "WHERE r.owner_id = #{ownerId} OR e.flag = 1 ORDER BY r.id") + List findPage(Page page, @Param("ownerId") Long ownerId); + + @Select("SELECT id, owner_id, name FROM audit_record WHERE id = #{firstId} OR id = #{secondId} ORDER BY id") + List findByEitherId(@Param("firstId") Long firstId, @Param("secondId") Long secondId); + + @Update("UPDATE audit_record SET name = 'changed' WHERE id = #{firstId} OR id = #{secondId}") + int renameEitherId(@Param("firstId") Long firstId, @Param("secondId") Long secondId); + } + + public interface AuditExtraMapper extends BaseMapperPlus { + } + + @TableName("audit_record") + public static class AuditRecord { + + @TableId + private Long id; + private Long ownerId; + private String name; + + public Long getId() { + return id; + } + + public Long getOwnerId() { + return ownerId; + } + + public String getName() { + return name; + } + } + + @TableName("audit_extra") + public static class AuditExtra { + + @TableId + private Long recordId; + private Integer flag; + + public Long getRecordId() { + return recordId; + } + + public Integer getFlag() { + return flag; + } + } +} diff --git a/ruoyi-common/ruoyi-common-mybatis/src/test/java/org/dromara/common/mybatis/interceptor/SqlLogInterceptorTest.java b/ruoyi-common/ruoyi-common-mybatis/src/test/java/org/dromara/common/mybatis/interceptor/SqlLogInterceptorTest.java new file mode 100644 index 000000000..fb906a695 --- /dev/null +++ b/ruoyi-common/ruoyi-common-mybatis/src/test/java/org/dromara/common/mybatis/interceptor/SqlLogInterceptorTest.java @@ -0,0 +1,84 @@ +package org.dromara.common.mybatis.interceptor; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.MybatisSqlSessionFactoryBuilder; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.datasource.unpooled.UnpooledDataSource; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; +import org.dromara.common.mybatis.config.properties.SqlLogProperties; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.Statement; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SqlLogInterceptorTest { + + @Test + void ignoredTableStillExecutesAndOtherTableStillLogs() throws Exception { + SqlLogProperties properties = new SqlLogProperties(); + properties.setIgnoreTables(List.of("flow_event_wait")); + SqlLogInterceptor interceptor = new SqlLogInterceptor(properties); + String url = "jdbc:h2:mem:sql_log_" + UUID.randomUUID().toString().replace("-", "") + ";DB_CLOSE_DELAY=-1"; + UnpooledDataSource dataSource = new UnpooledDataSource("org.h2.Driver", url, "sa", ""); + try (Connection connection = dataSource.getConnection(); Statement statement = connection.createStatement()) { + statement.execute("CREATE TABLE flow_event_wait (id BIGINT PRIMARY KEY)"); + statement.execute("CREATE TABLE audit_record (id BIGINT PRIMARY KEY)"); + statement.execute("INSERT INTO flow_event_wait VALUES (1)"); + statement.execute("INSERT INTO audit_record VALUES (2)"); + } + + MybatisConfiguration configuration = new MybatisConfiguration(); + configuration.setEnvironment(new Environment("h2", new JdbcTransactionFactory(), dataSource)); + configuration.addInterceptor(interceptor); + configuration.addMapper(LogMapper.class); + SqlSessionFactory factory = new MybatisSqlSessionFactoryBuilder().build(configuration); + PrintStream originalErr = System.err; + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try (PrintStream capture = new PrintStream(output, true, StandardCharsets.UTF_8)) { + System.setErr(capture); + try (SqlSession session = factory.openSession()) { + assertEquals(1L, session.getMapper(LogMapper.class).eventWaitId()); + assertEquals(2L, session.getMapper(LogMapper.class).auditId()); + } + } finally { + System.setErr(originalErr); + } + String logs = output.toString(StandardCharsets.UTF_8); + assertFalse(logs.contains("flow_event_wait")); + assertTrue(logs.contains("audit_record")); + } + + @Test + void matchesTableReferencesWithoutMatchingLiteralsOrSimilarNames() { + SqlLogProperties properties = new SqlLogProperties(); + properties.setIgnoreTables(List.of("flow_event_wait")); + SqlLogInterceptor interceptor = new SqlLogInterceptor(properties); + + assertTrue(interceptor.isIgnoredTableSql("select * from flow_event_wait where id = ?")); + assertTrue(interceptor.isIgnoredTableSql("select * from `app`.`flow_event_wait`")); + assertTrue(interceptor.isIgnoredTableSql("select * from audit_record join FLOW_EVENT_WAIT w on w.id = audit_record.id")); + assertTrue(interceptor.isIgnoredTableSql("update flow_event_wait set status = ?")); + assertFalse(interceptor.isIgnoredTableSql("select 'from flow_event_wait' from audit_record")); + assertFalse(interceptor.isIgnoredTableSql("select * from flow_event_wait_history")); + } + + interface LogMapper { + @Select("SELECT id FROM flow_event_wait") + Long eventWaitId(); + + @Select("SELECT id FROM audit_record") + Long auditId(); + } +} diff --git a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/core/SseEmitterSessionManager.java b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/core/SseEmitterSessionManager.java index df6365e5c..d3b32a90b 100644 --- a/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/core/SseEmitterSessionManager.java +++ b/ruoyi-common/ruoyi-common-push/src/main/java/org/dromara/common/push/core/SseEmitterSessionManager.java @@ -1,6 +1,5 @@ package org.dromara.common.push.core; -import cn.hutool.core.collection.CollUtil; import cn.hutool.core.map.MapUtil; import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.utils.ThreadUtils; @@ -19,6 +18,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; /** @@ -58,52 +58,41 @@ public class SseEmitterSessionManager implements PushSessionManager { * @return 返回一个 SseEmitter 实例,客户端可以通过该实例接收 SSE 事件 */ public SseEmitter connect(Long userId, String token) { - // 从 USER_TOKEN_EMITTERS 中获取或创建当前用户的 SseEmitter 映射表(ConcurrentHashMap) - // 每个用户可以有多个 SSE 连接,通过 token 进行区分 - Map emitters = USER_TOKEN_EMITTERS.computeIfAbsent(userId, k -> new ConcurrentHashMap<>()); - - // 关闭已存在的SseEmitter,防止超过最大连接数 - SseEmitter oldEmitter = emitters.remove(token); - if (oldEmitter != null) { - sendKickedMessage(oldEmitter); - oldEmitter.complete(); - } - - // 创建一个新的 SseEmitter 实例,避免连接之后直接关闭浏览器导致连接停滞 SseEmitter emitter = new SseEmitter(messageProperties.getSseTimeout()); + emitter.onCompletion(() -> removeEmitter(userId, token, emitter)); + emitter.onTimeout(() -> removeEmitter(userId, token, emitter)); + emitter.onError(e -> removeEmitter(userId, token, emitter)); - emitters.put(token, emitter); - - // 当 emitter 完成、超时或发生错误时,从映射表中移除对应的 token - emitter.onCompletion(() -> { - SseEmitter remove = emitters.remove(token); - if (remove != null) { - remove.complete(); - } - }); - emitter.onTimeout(() -> { - SseEmitter remove = emitters.remove(token); - if (remove != null) { - remove.complete(); - } - }); - emitter.onError((e) -> { - SseEmitter remove = emitters.remove(token); - if (remove != null) { - remove.complete(); + AtomicReference oldEmitter = new AtomicReference<>(); + USER_TOKEN_EMITTERS.compute(userId, (id, emitters) -> { + if (emitters == null) { + emitters = new ConcurrentHashMap<>(); } + oldEmitter.set(emitters.put(token, emitter)); + return emitters; }); + if (oldEmitter.get() != null) { + sendKickedMessage(oldEmitter.get()); + oldEmitter.get().complete(); + } try { // 向客户端发送一条连接成功的事件 emitter.send(SseEmitter.event().comment("connected")); } catch (IOException e) { - // 如果发送消息失败,则从映射表中移除 emitter - emitters.remove(token); + removeEmitter(userId, token, emitter); + emitter.complete(); } return emitter; } + private void removeEmitter(Long userId, String token, SseEmitter emitter) { + USER_TOKEN_EMITTERS.computeIfPresent(userId, (id, emitters) -> { + emitters.remove(token, emitter); + return emitters.isEmpty() ? null : emitters; + }); + } + /** * 通知旧连接已被同 token 新连接替换。 * @@ -129,17 +118,18 @@ public class SseEmitterSessionManager implements PushSessionManager { if (userId == null || token == null) { return; } - Map emitters = USER_TOKEN_EMITTERS.get(userId); - if (MapUtil.isNotEmpty(emitters)) { + AtomicReference removed = new AtomicReference<>(); + USER_TOKEN_EMITTERS.computeIfPresent(userId, (id, emitters) -> { + removed.set(emitters.remove(token)); + return emitters.isEmpty() ? null : emitters; + }); + SseEmitter sseEmitter = removed.get(); + if (sseEmitter != null) { try { - SseEmitter sseEmitter = emitters.get(token); sseEmitter.send(SseEmitter.event().comment("disconnected")); - sseEmitter.complete(); } catch (Exception ignore) { } - emitters.remove(token); - } else { - USER_TOKEN_EMITTERS.remove(userId); + sseEmitter.complete(); } } @@ -148,15 +138,7 @@ public class SseEmitterSessionManager implements PushSessionManager { */ public void sseMonitor() { final SseEmitter.SseEventBuilder heartbeat = SseEmitter.event().comment("heartbeat"); - // 记录需要移除的用户ID - List toRemoveUsers = new ArrayList<>(); - USER_TOKEN_EMITTERS.forEach((userId, emitterMap) -> { - if (CollUtil.isEmpty(emitterMap)) { - toRemoveUsers.add(userId); - return; - } - emitterMap.entrySet().removeIf(entry -> { try { entry.getValue().send(heartbeat); @@ -171,14 +153,8 @@ public class SseEmitterSessionManager implements PushSessionManager { } }); - // 移除空连接用户 - if (emitterMap.isEmpty()) { - toRemoveUsers.add(userId); - } + USER_TOKEN_EMITTERS.computeIfPresent(userId, (id, current) -> current.isEmpty() ? null : current); }); - - // 循环结束后统一清理空用户,避免并发修改异常 - toRemoveUsers.forEach(USER_TOKEN_EMITTERS::remove); } /** @@ -206,14 +182,13 @@ public class SseEmitterSessionManager implements PushSessionManager { .name("message") .data(message)); } catch (Exception e) { - SseEmitter remove = emitters.remove(entry.getKey()); - if (remove != null) { - remove.complete(); + if (emitters.remove(entry.getKey(), entry.getValue())) { + entry.getValue().complete(); } } } } else { - USER_TOKEN_EMITTERS.remove(userId); + USER_TOKEN_EMITTERS.computeIfPresent(userId, (id, current) -> current.isEmpty() ? null : current); } } diff --git a/ruoyi-common/ruoyi-common-push/src/test/java/org/dromara/common/push/PushFunctionTest.java b/ruoyi-common/ruoyi-common-push/src/test/java/org/dromara/common/push/PushFunctionTest.java index da5f04cb2..bf4e22cb6 100644 --- a/ruoyi-common/ruoyi-common-push/src/test/java/org/dromara/common/push/PushFunctionTest.java +++ b/ruoyi-common/ruoyi-common-push/src/test/java/org/dromara/common/push/PushFunctionTest.java @@ -3,13 +3,19 @@ package org.dromara.common.push; import org.dromara.common.push.annotation.ConditionalOnMessageTransport; import org.dromara.common.push.condition.MessageTransportCondition; import org.dromara.common.push.enums.MessageTransportEnum; +import org.dromara.common.push.core.SseEmitterSessionManager; +import org.dromara.common.push.properties.MessageProperties; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.mock.env.MockEnvironment; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledExecutorService; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -52,4 +58,28 @@ class PushFunctionTest { environment.setProperty("message.enabled", "false"); assertFalse(condition.matches(context, metadata)); } + + @Test + @DisplayName("旧 SSE 回调不移除重连后的新连接") + @SuppressWarnings("unchecked") + void shouldKeepReplacementEmitterWhenOldConnectionCompletes() { + ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class); + SseEmitterSessionManager manager = new SseEmitterSessionManager(scheduler, new MessageProperties()); + Map> sessions = + (Map>) ReflectionTestUtils.getField(SseEmitterSessionManager.class, "USER_TOKEN_EMITTERS"); + long userId = 999_999L; + String token = "replacement-test"; + SseEmitter oldEmitter = new SseEmitter(); + SseEmitter newEmitter = new SseEmitter(); + sessions.put(userId, new ConcurrentHashMap<>(Map.of(token, newEmitter))); + try { + ReflectionTestUtils.invokeMethod(manager, "removeEmitter", userId, token, oldEmitter); + assertEquals(newEmitter, sessions.get(userId).get(token)); + + ReflectionTestUtils.invokeMethod(manager, "removeEmitter", userId, token, newEmitter); + assertFalse(sessions.containsKey(userId)); + } finally { + sessions.remove(userId); + } + } } diff --git a/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/handler/SaTokenExceptionHandler.java b/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/handler/SaTokenExceptionHandler.java index bdd9a5ab4..cb81e6e95 100644 --- a/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/handler/SaTokenExceptionHandler.java +++ b/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/handler/SaTokenExceptionHandler.java @@ -7,6 +7,8 @@ import cn.hutool.http.HttpStatus; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.domain.R; +import org.springframework.http.ResponseEntity; +import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -19,6 +21,9 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class SaTokenExceptionHandler { + @Value("${message.path:/resource/message}") + private String messagePath = "/resource/message"; + /** * 处理权限码校验失败异常。 * @@ -42,9 +47,12 @@ public class SaTokenExceptionHandler { * @return 统一失败响应 */ @ExceptionHandler(NotLoginException.class) - public R handleNotLoginException(NotLoginException e, HttpServletRequest request) { + public ResponseEntity handleNotLoginException(NotLoginException e, HttpServletRequest request) { String requestURI = request.getRequestURI(); - log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, e.getMessage()); + log.warn("请求地址'{}',认证失败,类型'{}'", requestURI, e.getType()); + if (request.getServletPath().equals(messagePath)) { + return ResponseEntity.status(HttpStatus.HTTP_UNAUTHORIZED).build(); + } String msg = switch (e.getType()) { case NotLoginException.TOKEN_TIMEOUT, NotLoginException.TOKEN_FREEZE -> "登录已过期,请重新登录"; @@ -52,7 +60,7 @@ public class SaTokenExceptionHandler { case NotLoginException.KICK_OUT -> "账号已被管理员强制下线"; default -> "登录状态异常,请重新登录"; }; - return R.fail(HttpStatus.HTTP_UNAUTHORIZED, msg); + return ResponseEntity.ok(R.fail(HttpStatus.HTTP_UNAUTHORIZED, msg)); } } diff --git a/ruoyi-common/ruoyi-common-satoken/src/test/java/org/dromara/common/satoken/SaTokenFunctionTest.java b/ruoyi-common/ruoyi-common-satoken/src/test/java/org/dromara/common/satoken/SaTokenFunctionTest.java index 44be2eaa3..9e1a396f8 100644 --- a/ruoyi-common/ruoyi-common-satoken/src/test/java/org/dromara/common/satoken/SaTokenFunctionTest.java +++ b/ruoyi-common/ruoyi-common-satoken/src/test/java/org/dromara/common/satoken/SaTokenFunctionTest.java @@ -55,8 +55,22 @@ class SaTokenFunctionTest { NotLoginException timeout = NotLoginException.newInstance("login", NotLoginException.TOKEN_TIMEOUT, "timeout", "token"); NotLoginException replaced = NotLoginException.newInstance("login", NotLoginException.BE_REPLACED, "replaced", "token"); - assertEquals("登录已过期,请重新登录", handler.handleNotLoginException(timeout, request).getMsg()); - assertEquals("当前账号已在其他设备登录,您已被强制下线", handler.handleNotLoginException(replaced, request).getMsg()); + assertEquals("登录已过期,请重新登录", ((R) handler.handleNotLoginException(timeout, request).getBody()).getMsg()); + assertEquals("当前账号已在其他设备登录,您已被强制下线", ((R) handler.handleNotLoginException(replaced, request).getBody()).getMsg()); + } + + @Test + @DisplayName("SSE 登录失效返回无正文 401") + void shouldRejectSseWithoutJsonConversion() { + SaTokenExceptionHandler handler = new SaTokenExceptionHandler(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/resource/message"); + request.setServletPath("/resource/message"); + NotLoginException expired = NotLoginException.newInstance("login", NotLoginException.TOKEN_TIMEOUT, "timeout", "token"); + + var response = handler.handleNotLoginException(expired, request); + + assertEquals(HttpStatus.HTTP_UNAUTHORIZED, response.getStatusCode().value()); + assertEquals(null, response.getBody()); } /** diff --git a/ruoyi-extend/ruoyi-monitor-admin/Dockerfile b/ruoyi-extend/ruoyi-monitor-admin/Dockerfile index 2adc8f18c..6089a5b69 100644 --- a/ruoyi-extend/ruoyi-monitor-admin/Dockerfile +++ b/ruoyi-extend/ruoyi-monitor-admin/Dockerfile @@ -1,6 +1,6 @@ # 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/ -FROM bellsoft/liberica-openjdk-rocky:21.0.12-cds -# FROM bellsoft/liberica-openjdk-rocky:25.0.4-cds +FROM bellsoft/liberica-openjdk-rocky:21.0.13-cds +# FROM bellsoft/liberica-openjdk-rocky:25.0.5-cds # FROM findepi/graalvm:java21-native LABEL maintainer="Lion Li" diff --git a/ruoyi-extend/ruoyi-snailai-server/Dockerfile b/ruoyi-extend/ruoyi-snailai-server/Dockerfile index e3b4dc261..912624f6f 100644 --- a/ruoyi-extend/ruoyi-snailai-server/Dockerfile +++ b/ruoyi-extend/ruoyi-snailai-server/Dockerfile @@ -1,6 +1,6 @@ # 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/ -FROM bellsoft/liberica-openjdk-rocky:21.0.12-cds -# FROM bellsoft/liberica-openjdk-rocky:25.0.4-cds +FROM bellsoft/liberica-openjdk-rocky:21.0.13-cds +# FROM bellsoft/liberica-openjdk-rocky:25.0.5-cds # FROM findepi/graalvm:java21-native LABEL maintainer="Lion Li" diff --git a/ruoyi-extend/ruoyi-snailjob-server/Dockerfile b/ruoyi-extend/ruoyi-snailjob-server/Dockerfile index 266edb4c6..f5360afad 100644 --- a/ruoyi-extend/ruoyi-snailjob-server/Dockerfile +++ b/ruoyi-extend/ruoyi-snailjob-server/Dockerfile @@ -1,6 +1,6 @@ # 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/ -FROM bellsoft/liberica-openjdk-rocky:21.0.12-cds -# FROM bellsoft/liberica-openjdk-rocky:25.0.4-cds +FROM bellsoft/liberica-openjdk-rocky:21.0.13-cds +# FROM bellsoft/liberica-openjdk-rocky:25.0.5-cds # FROM findepi/graalvm:java21-native LABEL maintainer="Lion Li" diff --git a/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/service/GenTableServiceImpl.java b/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/service/GenTableServiceImpl.java index bfd5ff909..b15b6baa2 100644 --- a/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/service/GenTableServiceImpl.java +++ b/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/service/GenTableServiceImpl.java @@ -130,7 +130,7 @@ public class GenTableServiceImpl implements IGenTableService { String tableName = genTable.getTableName(); String tableComment = genTable.getTableComment(); - LinkedHashMap> tablesMap = ServiceProxy.metadata().tables(); + Map> tablesMap = ServiceProxy.metadata().tables(); if (CollUtil.isEmpty(tablesMap)) { return PageResult.build(); } @@ -195,13 +195,13 @@ public class GenTableServiceImpl implements IGenTableService { @Override public List selectDbTableListByNames(String[] tableNames, String dataName) { Set tableNameSet = new HashSet<>(List.of(tableNames)); - LinkedHashMap> tablesMap = ServiceProxy.metadata().tables(); + Map> tablesMap = ServiceProxy.metadata().tables(); if (CollUtil.isEmpty(tablesMap)) { return new ArrayList<>(); } - List> tableList = tablesMap.values().stream() + List> tableList = tablesMap.values().stream() .filter(x -> !StringUtils.startWithAnyIgnoreCase(x.getName(), TABLE_IGNORE)) .filter(x -> tableNameSet.contains(x.getName())).toList(); @@ -340,9 +340,11 @@ public class GenTableServiceImpl implements IGenTableService { @Override public byte[] downloadCode(Long tableId) { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - ZipOutputStream zip = new ZipOutputStream(outputStream); - writeCodeToZip(tableId, zip); - IoUtil.close(zip); + try(ZipOutputStream zip = new ZipOutputStream(outputStream)) { + writeCodeToZip(tableId, zip); + } catch (IOException e) { + log.error("渲染模板失败,Zip输出流IO异常。", e); + } return outputStream.toByteArray(); } @@ -407,11 +409,13 @@ public class GenTableServiceImpl implements IGenTableService { @Override public byte[] downloadCode(String[] tableIds) { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - ZipOutputStream zip = new ZipOutputStream(outputStream); - for (String tableId : tableIds) { - writeCodeToZip(Long.parseLong(tableId), zip); + try(ZipOutputStream zip = new ZipOutputStream(outputStream)) { + for (String tableId : tableIds) { + writeCodeToZip(Long.parseLong(tableId), zip); + } + } catch (IOException e) { + log.error("渲染模板失败,Zip输出流IO异常。", e); } - IoUtil.close(zip); return outputStream.toByteArray(); } diff --git a/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/util/GenUtils.java b/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/util/GenUtils.java index 3722d9851..196eada56 100644 --- a/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/util/GenUtils.java +++ b/ruoyi-modules/ruoyi-gen/src/main/java/org/dromara/gen/util/GenUtils.java @@ -278,11 +278,8 @@ public class GenUtils { * @return 业务名 */ public static String getBusinessName(String tableName) { - int firstIndex = tableName.indexOf("_"); - int nameLength = tableName.length(); - String businessName = StringUtils.substring(tableName, firstIndex + 1, nameLength); - businessName = StringUtils.toCamelCase(businessName); - return businessName; + // 业务名与类名共用表前缀处理规则,仅需转为首字母小写。 + return StringUtils.uncapitalize(convertClassName(tableName)); } /** diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysUserBo.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysUserBo.java index 93ce15029..6e3036507 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysUserBo.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysUserBo.java @@ -3,9 +3,11 @@ package org.dromara.system.domain.bo; import io.github.linpeilie.annotations.AutoMapper; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; import lombok.Data; import lombok.NoArgsConstructor; +import org.dromara.common.core.constant.RegexConstants; import org.dromara.common.core.constant.SystemConstants; import org.dromara.common.core.xss.Xss; import org.dromara.system.domain.SysUser; @@ -70,6 +72,7 @@ public class SysUserBo implements Serializable { /** * 手机号码 */ + @Pattern(regexp = RegexConstants.MOBILE, message = "手机号格式不正确") private String phoneNumber; /** diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysUserProfileBo.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysUserProfileBo.java index 2b6ee093b..4af9bf7a2 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysUserProfileBo.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysUserProfileBo.java @@ -7,8 +7,6 @@ import lombok.Data; import lombok.NoArgsConstructor; import org.dromara.common.core.constant.RegexConstants; import org.dromara.common.core.xss.Xss; -import org.dromara.common.sensitive.annotation.Sensitive; -import org.dromara.common.sensitive.core.SensitiveStrategy; import java.io.Serial; import java.io.Serializable; @@ -36,7 +34,6 @@ public class SysUserProfileBo implements Serializable { /** * 用户邮箱 */ - @Sensitive(strategy = SensitiveStrategy.EMAIL) @Email(message = "邮箱格式不正确") @Size(min = 0, max = 50, message = "邮箱长度不能超过{max}个字符") private String email; @@ -45,7 +42,6 @@ public class SysUserProfileBo implements Serializable { * 手机号码 */ @Pattern(regexp = RegexConstants.MOBILE, message = "手机号格式不正确") - @Sensitive(strategy = SensitiveStrategy.PHONE) private String phoneNumber; /** diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/runner/SystemApplicationRunner.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/runner/SystemApplicationRunner.java index 9830b23cc..d0b8ed9c9 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/runner/SystemApplicationRunner.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/runner/SystemApplicationRunner.java @@ -27,7 +27,6 @@ public class SystemApplicationRunner implements ApplicationRunner { @Override public void run(ApplicationArguments args) throws Exception { ossConfigService.init(); - log.info("初始化OSS配置成功"); } } diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysDeptServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysDeptServiceImpl.java index 700f1068f..1a5aa7793 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysDeptServiceImpl.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysDeptServiceImpl.java @@ -180,9 +180,11 @@ public class SysDeptServiceImpl implements ISysDeptService, DeptService { @Override public List selectDeptByIds(Collection deptIds) { return deptMapper.selectDeptList(deptMapper.lambda() - .select(SysDept::getDeptId, SysDept::getDeptName, SysDept::getLeader) + .select(SysDept::getDeptId, SysDept::getDeptName, SysDept::getLeader, + SysDept::getDeptCategory, SysDept::getParentId, SysDept::getAncestors) .eq(SysDept::getStatus, SystemConstants.NORMAL) .in(CollUtil.isNotEmpty(deptIds), SysDept::getDeptId, deptIds) + .orderByAsc(SysDept::getAncestors, SysDept::getParentId, SysDept::getOrderNum, SysDept::getDeptId) .build()); } diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssConfigServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssConfigServiceImpl.java index f5e74e3b8..83943a1d8 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssConfigServiceImpl.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssConfigServiceImpl.java @@ -51,14 +51,31 @@ public class SysOssConfigServiceImpl implements ISysOssConfigService { */ @Override public void init() { - List list = ossConfigMapper.selectList(); - // 加载OSS初始化配置 - for (SysOssConfig config : list) { - String configKey = config.getConfigKey(); - if (SystemConstants.YES.equals(config.getStatus())) { - RedisUtils.setCacheObject(OssConstant.DEFAULT_CONFIG_KEY, configKey); + try { + List list = ossConfigMapper.selectList(); + if (CollUtil.isEmpty(list)) { + log.warn("OSS配置初始化未查询到配置数据"); + return; } - CacheUtils.put(CacheNames.SYS_OSS_CONFIG, config.getConfigKey(), JsonUtils.toJsonString(config)); + + boolean hasDefaultConfig = false; + // 加载OSS初始化配置 + for (SysOssConfig config : list) { + String configKey = config.getConfigKey(); + if (SystemConstants.YES.equals(config.getStatus())) { + RedisUtils.setCacheObject(OssConstant.DEFAULT_CONFIG_KEY, configKey); + hasDefaultConfig = true; + } + CacheUtils.put(CacheNames.SYS_OSS_CONFIG, configKey, JsonUtils.toJsonString(config)); + } + if (!hasDefaultConfig) { + log.warn("OSS配置初始化未找到启用的默认配置"); + } else { + log.info("初始化OSS配置成功"); + } + } catch (RuntimeException e) { + log.error("OSS配置初始化失败", e); + throw e; } } diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java index fce63b11e..5a6fb4116 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java @@ -552,10 +552,10 @@ public class SysUserServiceImpl implements ISysUserService, UserService { */ @Override public List selectUserListByDept(Long deptId) { - return userMapper.lambda() + return userMapper.selectUserList(userMapper.lambda() .eq(SysUser::getDeptId, deptId) .orderByAsc(SysUser::getUserId) - .voList(); + .build()); } /** @@ -690,8 +690,12 @@ public class SysUserServiceImpl implements ISysUserService, UserService { if (CollUtil.isEmpty(roleIds)) { return List.of(); } - List userRoles = userRoleMapper.lambda().in(SysUserRole::getRoleId, roleIds).list(); - return StreamUtils.toList(userRoles, SysUserRole::getUserId); + List userRoles = userRoleMapper.lambda() + .select(SysUserRole::getUserId) + .in(SysUserRole::getRoleId, roleIds) + .list(); + Set userIds = StreamUtils.toSet(userRoles, SysUserRole::getUserId); + return new ArrayList<>(userIds); } /** @@ -702,17 +706,8 @@ public class SysUserServiceImpl implements ISysUserService, UserService { */ @Override public List selectUsersByRoleIds(Collection roleIds) { - if (CollUtil.isEmpty(roleIds)) { - return List.of(); - } - - // 通过角色ID获取用户角色信息 - List userRoles = userRoleMapper.lambda().in(SysUserRole::getRoleId, roleIds).list(); - - // 获取用户ID列表 - Set userIds = StreamUtils.toSet(userRoles, SysUserRole::getUserId); - - return this.selectListByIds(new ArrayList<>(userIds)); + List userIds = this.selectUserIdsByRoleIds(roleIds); + return this.selectListByIds(userIds); } /** @@ -726,14 +721,33 @@ public class SysUserServiceImpl implements ISysUserService, UserService { if (CollUtil.isEmpty(deptIds)) { return List.of(); } - List list = userMapper.lambda() + List list = userMapper.lambda() .select(SysUser::getUserId, SysUser::getUserName, SysUser::getNickName, SysUser::getEmail, SysUser::getPhoneNumber) .eq(SysUser::getStatus, SystemConstants.NORMAL) .in(SysUser::getDeptId, deptIds) - .voList(); + .list(); return BeanUtil.copyToList(list, UserDTO.class); } + /** + * 通过部门ID查询用户ID + * + * @param deptIds 部门ids + * @return 用户ids + */ + @Override + public List selectUserIdsByDeptIds(Collection deptIds) { + if (CollUtil.isEmpty(deptIds)) { + return List.of(); + } + List list = userMapper.lambda() + .select(SysUser::getUserId) + .eq(SysUser::getStatus, SystemConstants.NORMAL) + .in(SysUser::getDeptId, deptIds) + .list(); + return StreamUtils.toList(list, SysUser::getUserId); + } + /** * 通过岗位ID查询用户 * @@ -742,17 +756,27 @@ public class SysUserServiceImpl implements ISysUserService, UserService { */ @Override public List selectUsersByPostIds(Collection postIds) { + List userIds = this.selectUserIdsByPostIds(postIds); + return this.selectListByIds(userIds); + } + + /** + * 通过岗位ID查询用户ID + * + * @param postIds 岗位ids + * @return 用户ids + */ + @Override + public List selectUserIdsByPostIds(Collection postIds) { if (CollUtil.isEmpty(postIds)) { return List.of(); } - - // 通过岗位ID获取用户岗位信息 - List userPosts = userPostMapper.lambda().in(SysUserPost::getPostId, postIds).list(); - - // 获取用户ID列表 + List userPosts = userPostMapper.lambda() + .select(SysUserPost::getUserId) + .in(SysUserPost::getPostId, postIds) + .list(); Set userIds = StreamUtils.toSet(userPosts, SysUserPost::getUserId); - - return this.selectListByIds(new ArrayList<>(userIds)); + return new ArrayList<>(userIds); } /** diff --git a/ruoyi-modules/ruoyi-workflow/pom.xml b/ruoyi-modules/ruoyi-workflow/pom.xml index 814de3e96..23e2e2675 100644 --- a/ruoyi-modules/ruoyi-workflow/pom.xml +++ b/ruoyi-modules/ruoyi-workflow/pom.xml @@ -86,13 +86,13 @@ - org.dromara.warm + io.github.sgs98 warm-flow-mybatis-plus-sb4-starter - org.dromara.warm + io.github.sgs98 warm-flow-plugin-ui-sb-web diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/constant/FlowConstant.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/constant/FlowConstant.java index 512a1348f..f50e9ebd3 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/constant/FlowConstant.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/constant/FlowConstant.java @@ -83,11 +83,6 @@ public interface FlowConstant { */ String WF_TASK_STATUS = "wf_task_status"; - /** - * 自动通过变量标识。 - */ - String AUTO_PASS = "autoPass"; - /** * 业务编码流程变量名。 */ diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/enums/CopySettingEnum.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/enums/CopySettingEnum.java deleted file mode 100644 index 5512eb9ec..000000000 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/enums/CopySettingEnum.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.dromara.workflow.common.enums; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -/** - * 抄送设置枚举 - * - * @author AprilWind - */ -@Getter -@AllArgsConstructor -public enum CopySettingEnum implements NodeExtEnum { - ; - - /** - * 展示名称。 - */ - private final String label; - - /** - * 配置值。 - */ - private final String value; - - /** - * 是否默认选中。 - */ - private final boolean selected; - -} - diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/enums/TaskStatusEnum.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/enums/TaskStatusEnum.java deleted file mode 100644 index 19807dd96..000000000 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/enums/TaskStatusEnum.java +++ /dev/null @@ -1,115 +0,0 @@ -package org.dromara.workflow.common.enums; - -import cn.hutool.core.util.StrUtil; -import lombok.AllArgsConstructor; -import lombok.Getter; - -import java.util.Arrays; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * 任务状态枚举 - * - * @author may - */ -@Getter -@AllArgsConstructor -public enum TaskStatusEnum { - - /** - * 撤销 - */ - CANCEL("cancel", "撤销"), - - /** - * 通过 - */ - PASS("pass", "通过"), - - /** - * 待审核 - */ - WAITING("waiting", "待审核"), - - /** - * 作废 - */ - INVALID("invalid", "作废"), - - /** - * 退回 - */ - BACK("back", "退回"), - - /** - * 终止 - */ - TERMINATION("termination", "终止"), - - /** - * 转办 - */ - TRANSFER("transfer", "转办"), - - /** - * 委托 - */ - DEPUTE("depute", "委托"), - - /** - * 抄送 - */ - COPY("copy", "抄送"), - - /** - * 加签 - */ - SIGN("sign", "加签"), - - /** - * 减签 - */ - SIGN_OFF("sign_off", "减签"), - - /** - * 超时 - */ - TIMEOUT("timeout", "超时"); - - /** - * 状态编码与描述缓存。 - */ - private static final Map STATUS_DESC_MAP = Arrays.stream(values()) - .collect(Collectors.toConcurrentMap(TaskStatusEnum::getStatus, TaskStatusEnum::getDesc)); - /** - * 状态 - */ - private final String status; - /** - * 状态描述。 - */ - private final String desc; - - /** - * 根据状态编码获取任务业务状态描述。 - * - * @param status 状态 - * @return 状态描述 - */ - public static String findByStatus(String status) { - // 从缓存中直接获取描述 - return STATUS_DESC_MAP.getOrDefault(status, StrUtil.EMPTY); - } - - /** - * 判断状态是否为通过或退回 - * - * @param status 状态值 - * @return true 表示是通过或退回状态 - */ - public static boolean isPassOrBack(String status) { - return PASS.getStatus().equals(status) || BACK.getStatus().equals(status); - } - -} diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwEventWaitController.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwEventWaitController.java new file mode 100644 index 000000000..a7f8f704c --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwEventWaitController.java @@ -0,0 +1,110 @@ +package org.dromara.workflow.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import org.dromara.common.core.domain.PageResult; +import org.dromara.common.core.domain.R; +import org.dromara.common.log.annotation.Log; +import org.dromara.common.log.enums.BusinessType; +import org.dromara.common.redis.annotation.RepeatSubmit; +import org.dromara.common.web.core.BaseController; +import org.dromara.workflow.common.ConditionalOnEnable; +import org.dromara.workflow.domain.bo.FlowEventWaitBo; +import org.dromara.workflow.domain.bo.FlowTimerTriggerBo; +import org.dromara.workflow.domain.vo.FlowEventWaitVo; +import org.dromara.workflow.service.IFlwEventWaitService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +/** + * 定时任务(事件等待记录)控制器。 + *

数据来源为引擎事件等待表,默认只查定时类事件;消息类 / 错误类等待需显式放开事件类型范围。 + * 支持定时启动任务手动补发,以及已死事件等待记录复活;补发不推进等待记录, + * 定时任务的下次触发时间不受影响。

+ * + * @author may + * @since 2026/10/21 + */ +@ConditionalOnEnable +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/workflow/eventWait") +public class FlwEventWaitController extends BaseController { + + private final IFlwEventWaitService flwEventWaitService; + + /** + * 分页查询定时任务(事件等待记录)列表。 + * + * @param bo 查询条件 + * @return 等待记录分页数据 + */ + @SaCheckPermission("workflow:eventWait:list") + @GetMapping("/list") + public R> list(FlowEventWaitBo bo) { + return R.ok(flwEventWaitService.queryPageList(bo)); + } + + /** + * 按同一组筛选条件统计各状态等待记录数量。 + * + * @param bo 查询条件 + * @return 各状态计数 + */ + @SaCheckPermission("workflow:eventWait:list") + @GetMapping("/summary") + public R> summary(FlowEventWaitBo bo) { + return R.ok(flwEventWaitService.summary(bo)); + } + + /** + * 查询定时任务详情。 + * + * @param id 事件等待记录主键 + * @return 等待记录详情 + */ + @SaCheckPermission("workflow:eventWait:query") + @GetMapping("/{id}") + public R getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) { + return R.ok(flwEventWaitService.queryById(id)); + } + + /** + * 手动补发一次定时启动任务(运维应急)。 + *

不推进等待记录,下一次定时触发照常进行;实例发起人记为当前登录用户。

+ * + * @param bo 补发参数 + * @return 新建实例主键 + */ + @SaCheckPermission("workflow:eventWait:trigger") + @Log(title = "定时任务手动补发", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping("/trigger") + public R trigger(@RequestBody FlowTimerTriggerBo bo) { + return R.ok(flwEventWaitService.manualTrigger(bo)); + } + + /** + * 复活已死的事件等待记录,定时启动任务需通过重新发布或挂起后激活恢复。 + * + * @param id 事件等待记录主键 + * @return 操作结果 + */ + @SaCheckPermission("workflow:eventWait:trigger") + @Log(title = "事件等待记录复活", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PostMapping("/{id}/revive") + public R revive(@NotNull(message = "主键不能为空") @PathVariable Long id) { + flwEventWaitService.revive(id); + return R.ok(); + } +} diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwInstanceController.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwInstanceController.java index 389597560..70e6501ab 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwInstanceController.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwInstanceController.java @@ -171,6 +171,16 @@ public class FlwInstanceController extends BaseController { return R.ok(flwInstanceService.flowHisTaskList(businessId)); } + /** + * 根据流程实例 ID、业务 ID 或两者查询流程进度。 + */ + @GetMapping("/flowHisTaskList") + @SaCheckPermission("workflow:instance:query") + public R> flowHisTaskList(@RequestParam(required = false) Long instanceId, + @RequestParam(required = false) String businessId) { + return R.ok(flwInstanceService.flowHisTaskList(instanceId, businessId)); + } + /** * 获取流程变量。 * diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/demo/DemoClassServiceTask.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/demo/DemoClassServiceTask.java new file mode 100644 index 000000000..de3dc4cbd --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/demo/DemoClassServiceTask.java @@ -0,0 +1,32 @@ +package org.dromara.workflow.demo; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.warm.flow.core.service.task.ServiceTaskContext; +import org.dromara.warm.flow.core.service.task.ServiceTaskHandler; +import org.dromara.workflow.common.ConditionalOnEnable; +import org.springframework.stereotype.Component; + +/** + * 服务任务示例:class 模式 + *

+ * 节点配置 {@code node_config.serviceTask} 里写 {@code {"type":"class","handler":"本类全名"}}, + * 引擎先按全名从容器取 Bean,取不到才反射新建实例,所以注册成 Bean 后可以注入其它服务。 + * + * @author may + */ +@ConditionalOnEnable +@Slf4j +@Component +public class DemoClassServiceTask implements ServiceTaskHandler { + + @Override + public void execute(ServiceTaskContext context) { + log.info("服务任务[class 模式]开始执行: instanceId={}, nodeCode={}, nodeName={}", + context.getInstance().getId(), context.getNode().getNodeCode(), context.getNode().getNodeName()); + context.setVariable("classTaskNode", context.getNode().getNodeCode()); + context.setVariable("classTaskResult", "class模式已执行"); + log.info("服务任务[class 模式]执行完成: instanceId={}, nodeCode={}", + context.getInstance().getId(), context.getNode().getNodeCode()); + } + +} diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/demo/DemoDelegateServiceTask.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/demo/DemoDelegateServiceTask.java new file mode 100644 index 000000000..7b5b5d071 --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/demo/DemoDelegateServiceTask.java @@ -0,0 +1,38 @@ +package org.dromara.workflow.demo; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.warm.flow.core.service.task.ServiceTaskContext; +import org.dromara.warm.flow.core.service.task.ServiceTaskHandler; +import org.dromara.workflow.common.ConditionalOnEnable; +import org.springframework.stereotype.Component; + +/** + * 服务任务示例:delegateExpression 模式 + *

+ * 节点配置写 {@code {"type":"delegateExpression","expression":"#{@demoDelegateServiceTask}"}}, + * 引擎按 SpEL 从容器取出本 Bean 后调用 {@code execute}。Bean 名即表达式里的名字,改名会打断已发布的定义。 + * + * @author may + */ +@ConditionalOnEnable +@Slf4j +@Component("demoDelegateServiceTask") +@RequiredArgsConstructor +public class DemoDelegateServiceTask implements ServiceTaskHandler { + + private final DemoOrderService demoOrderService; + + @Override + public void execute(ServiceTaskContext context) { + log.info("服务任务[delegateExpression 模式]开始执行: instanceId={}, nodeCode={}", + context.getInstance().getId(), context.getNode().getNodeCode()); + String orderNo = demoOrderService.createOrder(context); + context.setVariable("delegateTaskOrderNo", orderNo); + context.setVariable("delegateTaskResult", StringUtils.format("delegateExpression模式已执行, 单号={}", orderNo)); + log.info("服务任务[delegateExpression 模式]执行完成: instanceId={}, orderNo={}", + context.getInstance().getId(), orderNo); + } + +} diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/demo/DemoOrderService.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/demo/DemoOrderService.java new file mode 100644 index 000000000..1e9a9898b --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/demo/DemoOrderService.java @@ -0,0 +1,60 @@ +package org.dromara.workflow.demo; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.warm.flow.core.service.task.ServiceTaskContext; +import org.dromara.workflow.common.ConditionalOnEnable; +import org.springframework.stereotype.Component; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * 服务任务示例:expression 模式的被调用方 + *

+ * 节点配置写 {@code {"type":"expression","expression":"#{@demoOrderService.createOrder(#execution)}", + * "resultVariable":"orderNo"}},{@code #execution} 就是引擎传入的 {@link ServiceTaskContext}, + * 返回值按 {@code resultVariable} 写回流程变量。 + * + * @author may + */ +@ConditionalOnEnable +@Slf4j +@Component("demoOrderService") +public class DemoOrderService { + + /** + * 示例单号序列,仅用于演示“服务任务产出变量供后续网关判断”,非业务落库。 + */ + private final AtomicLong sequence = new AtomicLong(1000L); + + /** + * 生成示例业务单号。 + * + * @param context 服务任务上下文 + * @return 示例单号 + */ + public String createOrder(ServiceTaskContext context) { + log.info("服务任务[expression 模式]开始执行: instanceId={}, nodeCode={}", + context.getInstance().getId(), context.getNode().getNodeCode()); + context.setVariable("expressionTaskNode", context.getNode().getNodeCode()); + context.setVariable("expressionTaskResult", "expression模式已执行"); + String orderNo = "SO" + sequence.incrementAndGet(); + log.info("服务任务[expression 模式]执行完成: instanceId={}, nodeCode={}, orderNo={}", + context.getInstance().getId(), context.getNode().getNodeCode(), orderNo); + return orderNo; + } + + /** + * 按入参变量算请假天数,产出变量供后面的网关条件(如 {@code gt@@day|3})判断。 + * + * @param context 服务任务上下文 + * @return 天数 + */ + public Integer evalDay(ServiceTaskContext context) { + Object inputDay = context.getVariable("inputDay"); + int day = inputDay == null ? 5 : Integer.parseInt(String.valueOf(inputDay)); + log.info("服务任务[expression 模式]计算天数: instanceId={}, nodeCode={}, inputDay={}, day={}", + context.getInstance().getId(), context.getNode().getNodeCode(), inputDay, day); + return day; + } + +} diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/demo/DemoPayFailListener.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/demo/DemoPayFailListener.java new file mode 100644 index 000000000..90d96372d --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/demo/DemoPayFailListener.java @@ -0,0 +1,96 @@ +package org.dromara.workflow.demo; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.warm.flow.core.FlowEngine; +import org.dromara.warm.flow.core.entity.Instance; +import org.dromara.warm.flow.core.entity.Task; +import org.dromara.warm.flow.core.spi.Listener; +import org.dromara.warm.flow.core.spi.ListenerVariable; +import org.dromara.workflow.common.ConditionalOnEnable; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.Map; + +/** + * 付款失败善后监听器(示例) + *

+ * 挂在「财务执行付款」节点的 create 监听上:付款金额超过限额就调 {@code TaskService#throwError} + * 抛出 {@code PAY_FAILED},由流程定义上的错误边界捕获并走善后分支。金额先取本次办理变量, + * 取不到再回落实例变量,因为监听器可能在变量合并前触发。 + *

+ * 节点监听器配置里写的是本类全名,改包名或类名要同步改已发布的流程定义。 + * + * @author may + */ +@ConditionalOnEnable +@Slf4j +@Component +public class DemoPayFailListener implements Listener { + + /** + * 付款金额变量名。 + */ + public static final String VAR_PAY_AMOUNT = "payAmount"; + + /** + * 免审限额,超过即视为付款失败需善后。 + */ + public static final BigDecimal PAY_AMOUNT_LIMIT = new BigDecimal("1000"); + + /** + * 错误码,需与流程定义上错误边界的错误码一致。 + */ + public static final String ERROR_CODE_PAY_FAILED = "PAY_FAILED"; + + @Override + public void notify(ListenerVariable listenerVariable) { + Task task = listenerVariable == null ? null : listenerVariable.getTask(); + if (task == null || task.getId() == null) { + return; + } + BigDecimal amount = readPayAmount(listenerVariable); + if (amount == null || amount.compareTo(PAY_AMOUNT_LIMIT) <= 0) { + return; + } + log.info("付款金额 {} 超过限额 {},抛出 {} 转入错误边界: instanceId={}, taskId={}", + amount, PAY_AMOUNT_LIMIT, ERROR_CODE_PAY_FAILED, task.getInstanceId(), task.getId()); + FlowEngine.taskService().throwError(task.getId(), ERROR_CODE_PAY_FAILED, + "付款金额 " + amount.toPlainString() + " 超过限额 " + PAY_AMOUNT_LIMIT.toPlainString() + ",转入善后处理"); + } + + /** + * 先取本次办理的变量,取不到再回落实例变量。 + * + * @param listenerVariable 监听器变量 + * @return 付款金额,未配置时返回 {@code null} + */ + private BigDecimal readPayAmount(ListenerVariable listenerVariable) { + BigDecimal amount = parseAmount(listenerVariable.getVariable()); + if (amount != null) { + return amount; + } + Instance instance = listenerVariable.getInstance(); + return parseAmount(instance == null ? null : instance.getVariableMap()); + } + + /** + * 从变量集合里解析金额,非法值按未配置处理。 + * + * @param variable 变量集合 + * @return 付款金额,解析失败返回 {@code null} + */ + private BigDecimal parseAmount(Map variable) { + if (variable == null || variable.get(VAR_PAY_AMOUNT) == null) { + return null; + } + String value = String.valueOf(variable.get(VAR_PAY_AMOUNT)); + if (!StringUtils.isNumeric(value)) { + log.warn("流程变量 {} 不是数字,跳过付款校验: {}", VAR_PAY_AMOUNT, value); + return null; + } + return new BigDecimal(value); + } + +} diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/demo/DemoTimerInspectHandler.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/demo/DemoTimerInspectHandler.java new file mode 100644 index 000000000..2e952b8e1 --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/demo/DemoTimerInspectHandler.java @@ -0,0 +1,68 @@ +package org.dromara.workflow.demo; + +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import lombok.extern.slf4j.Slf4j; +import org.dromara.warm.flow.core.FlowEngine; +import org.dromara.warm.flow.core.entity.EventWait; +import org.dromara.warm.flow.core.enums.EventType; +import org.dromara.warm.flow.core.service.impl.EventRuntimeHandler; +import org.dromara.warm.flow.core.timer.EventTaskHandler; +import org.dromara.warm.flow.core.workflow.context.WorkflowContext; +import org.dromara.workflow.common.ConditionalOnEnable; +import org.springframework.stereotype.Component; + +/** + * 定时启动示例:为流程实例提供可被业务页面解析的数字业务 ID。 + * + *

接入要点:实现 {@link EventTaskHandler} 后引擎内置默认处理器(EventRuntimeHandler) + * 自动让位,本类成为全部事件的处理入口——非本演示流程的事件必须委托默认处理器, + * 否则会覆盖引擎原生的定时启动与边界事件行为;等待记录的完成由默认三参编排 + * (handle 后执行 completion),实现里无需自行调用。

+ * + *

使用方式:发布含「定时启动」节点的定义(流程编码 {@link #DEMO_FLOW_CODE}), + * 开启 {@code warm-flow.timer-enabled}。

+ * + * @author may + */ +@Slf4j +@Component +@ConditionalOnEnable +public class DemoTimerInspectHandler implements EventTaskHandler { + + /** + * 演示流程编码:对应设计器发布的含「定时启动」节点的定义 + */ + public static final String DEMO_FLOW_CODE = "demo_timer_start"; + + /** + * 引擎内置默认处理器:兜底所有非本演示流程的事件 + */ + private final EventTaskHandler defaultHandler = new EventRuntimeHandler(); + + /** + * 定时调度入口(无上下文):统一处理。 + */ + @Override + public void handle(EventWait wait) { + if (isInspectTimerStart(wait)) { + // 这里应该使用业务的id demo使用自动生成id 需要用户自行替换 + String businessId = String.valueOf(IdWorker.getId()); + WorkflowContext context = WorkflowContext.create(); + FlowEngine.insService().startByTimer(businessId, wait.getDefinitionId(), + wait.getNodeCode(), null, context); + log.info("定时启动示例已自动发起审批: businessId={}", businessId); + return; + } + // 非本流程的事件:委托默认处理器,保持定时启动与边界事件原生行为 + defaultHandler.handle(wait); + } + + /** + * 只拦截本演示流程的定时启动事件。 + */ + private boolean isInspectTimerStart(EventWait wait) { + return EventType.TIMER_START.getValue().equals(wait.getEventType()) + && DEMO_FLOW_CODE.equals(FlowEngine.defService() + .getById(wait.getDefinitionId()).getFlowCode()); + } +} diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/FlowEventWaitBo.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/FlowEventWaitBo.java new file mode 100644 index 000000000..8f924060a --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/FlowEventWaitBo.java @@ -0,0 +1,136 @@ +package org.dromara.workflow.domain.bo; + +import lombok.Data; +import org.dromara.warm.flow.core.dto.EventWaitQuery; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +/** + * 定时任务(事件等待记录)查询业务对象。 + *

过滤维度与引擎 {@link EventWaitQuery} 一一对应:多值条件取「或」、不同条件之间取「且」; + * 事件类型留空时按 {@link #timerOnly} 决定是否只查定时类事件。数据来自引擎 flow_event_wait, + * 本对象只读,不含写操作字段。

+ * + * @author may + * @since 2026/10/21 + */ +@Data +public class FlowEventWaitBo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 页码 + */ + private Integer pageNum; + + /** + * 每页条数 + */ + private Integer pageSize; + + /** + * 排序字段(引擎白名单:id / nextTriggerAt / retryAt / createTime / updateTime / status) + */ + private String orderByColumn; + + /** + * 排序方向 asc / desc + */ + private String isAsc; + + /** + * 等待状态多选(WAITING / RETRY / RUNNING / DEAD / CANCELLED),逗号分隔,留空表示不限 + */ + private String statuses; + + /** + * 事件类型多选(TIMER_START / TIMER_CATCH / TIMER_BOUNDARY / …),逗号分隔, + * 留空时按 timerOnly 取默认范围 + */ + private String eventTypes; + + /** + * 是否只查定时类事件,默认 true + */ + private Boolean timerOnly = true; + + /** + * 流程定义编码 + */ + private String flowCode; + + /** + * 业务流程主键 + */ + private String businessId; + + /** + * 事件节点编码 + */ + private String nodeCode; + + /** + * 事件名称(消息名 / 错误码) + */ + private String eventName; + + /** + * 下次触发时间下界 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date nextTriggerAtFrom; + + /** + * 下次触发时间上界 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date nextTriggerAtTo; + + /** + * 转换为引擎查询条件。 + * + * @return 引擎只读查询条件 + */ + public EventWaitQuery toQuery() { + EventWaitQuery query = new EventWaitQuery(); + query.setPageNum(pageNum == null || pageNum < 1 ? 1 : pageNum); + query.setPageSize(pageSize == null || pageSize < 1 ? 10 : pageSize); + query.setStatuses(splitCsv(statuses)); + query.setEventTypes(splitCsv(eventTypes)); + query.setTimerOnly(!Boolean.FALSE.equals(timerOnly)); + query.setFlowCode(flowCode); + query.setBusinessId(businessId); + query.setNodeCode(nodeCode); + query.setEventName(eventName); + query.setNextTriggerAtFrom(nextTriggerAtFrom); + query.setNextTriggerAtTo(nextTriggerAtTo); + if (orderByColumn != null && !orderByColumn.isBlank()) { + query.setOrderBy(orderByColumn); + query.setIsAsc(isAsc); + } + return query; + } + + /** + * 逗号分隔串拆成引擎多值条件;空串返回 {@code null} 表示不限。 + * + * @param csv 逗号分隔串 + * @return 多值列表或 null + */ + private static List splitCsv(String csv) { + if (csv == null || csv.isBlank()) { + return null; + } + return Arrays.stream(csv.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .toList(); + } +} diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/FlowTimerTriggerBo.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/FlowTimerTriggerBo.java new file mode 100644 index 000000000..eb769eb21 --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/FlowTimerTriggerBo.java @@ -0,0 +1,37 @@ +package org.dromara.workflow.domain.bo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Map; + +/** + * 定时启动任务手动补发业务对象。 + *

只对 TIMER_START 事件的等待记录生效;补发只创建一次实例,不读取也不推进等待记录, + * 定时任务的下次触发时间不受影响。

+ * + * @author may + * @since 2026/10/21 + */ +@Data +public class FlowTimerTriggerBo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 事件等待记录主键 + */ + private Long waitId; + + /** + * 业务主键,留空时生成数字雪花 ID + */ + private String businessId; + + /** + * 额外流程变量,合并到节点默认变量之后 + */ + private Map variables; +} diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/context/CompleteTaskContext.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/context/CompleteTaskContext.java index c6f56a003..5d8d4458c 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/context/CompleteTaskContext.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/context/CompleteTaskContext.java @@ -1,7 +1,6 @@ package org.dromara.workflow.domain.context; import lombok.Data; -import org.dromara.warm.flow.core.dto.FlowParams; import org.dromara.warm.flow.core.entity.Instance; import org.dromara.warm.flow.orm.entity.FlowTask; import org.dromara.workflow.domain.bo.CompleteTaskBo; @@ -31,14 +30,5 @@ public class CompleteTaskContext { */ private Instance instance; - /** - * Warm-Flow 办理参数,由参数构建节点生成并供执行、自动审批节点复用。 - */ - private FlowParams flowParams; - - /** - * 流程实例是否开启自动审批,由参数构建节点从实例变量中解析。 - */ - private Boolean autoPass; } diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/context/TaskOperationContext.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/context/TaskOperationContext.java index 73fa2de4c..0cb7cc246 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/context/TaskOperationContext.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/context/TaskOperationContext.java @@ -1,7 +1,7 @@ package org.dromara.workflow.domain.context; import lombok.Data; -import org.dromara.warm.flow.core.dto.FlowParams; +import org.dromara.warm.flow.core.workflow.context.OperatorContext; import org.dromara.warm.flow.core.entity.Task; import org.dromara.warm.flow.orm.entity.FlowNode; import org.dromara.workflow.common.enums.TaskOperationEnum; @@ -33,9 +33,14 @@ public class TaskOperationContext { private TaskOperationEnum operation; /** - * Warm-Flow 操作参数。 + * 当前任务操作者。 */ - private FlowParams flowParams; + private OperatorContext operator; + + /** + * 当前操作说明。 + */ + private String message; /** * 当前任务。 diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/FlowEventWaitVo.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/FlowEventWaitVo.java new file mode 100644 index 000000000..97de74cec --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/FlowEventWaitVo.java @@ -0,0 +1,158 @@ +package org.dromara.workflow.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * 定时任务(事件等待记录)视图对象 flow_event_wait。 + *

字段来自引擎 flow_event_wait;flowName / businessId 由服务层按定义与实例主键装配, + * eventTypeName / statusName 为展示文案。

+ * + * @author may + * @since 2026/10/21 + */ +@Data +public class FlowEventWaitVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键id + */ + private Long id; + + /** + * 流程定义id + */ + private Long definitionId; + + /** + * 流程名称 + */ + private String flowName; + + /** + * 流程实例id(定时启动事件为空) + */ + private Long instanceId; + + /** + * 业务流程主键 + */ + private String businessId; + + /** + * 事件节点编码 + */ + private String nodeCode; + + /** + * 事件类型(TIMER_START / TIMER_CATCH / TIMER_BOUNDARY / …) + */ + private String eventType; + + /** + * 事件类型文案 + */ + private String eventTypeName; + + /** + * 事件名称(消息名 / 错误码) + */ + private String eventName; + + /** + * 边界事件关联的待办任务id + */ + private Long attachedTaskId; + + /** + * 定时触发策略(AFTER / FIXED / POINT_IN_TIME) + */ + private String triggerType; + + /** + * 触发间隔表达式 + */ + private String triggerInterval; + + /** + * 下次触发时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date nextTriggerAt; + + /** + * 时区 + */ + private String timezone; + + /** + * 已触发次数 + */ + private Integer repeatCount; + + /** + * 最大触发次数 + */ + private Integer maxRepeatCount; + + /** + * 等待状态(WAITING / RETRY / RUNNING / CANCELLED / DEAD / SUCCESS) + */ + private String status; + + /** + * 等待状态文案 + */ + private String statusName; + + /** + * 已重试次数 + */ + private Integer retryCount; + + /** + * 最大重试次数 + */ + private Integer maxRetryCount; + + /** + * 下次重试时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date retryAt; + + /** + * 载荷 + */ + private String payload; + + /** + * 最近一次错误 + */ + private String lastError; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 更新时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** + * 定时引擎扫描开关是否开启(warm-flow.timer-enabled)。 + * 关闭时等待记录仍会生成但不会被自动触发,页面据此给出提醒。 + */ + private Boolean timerEnabled; +} diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/FlowHisTaskVo.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/FlowHisTaskVo.java index 1733a701c..aa79f6a8d 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/FlowHisTaskVo.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/FlowHisTaskVo.java @@ -64,12 +64,12 @@ public class FlowHisTaskVo implements Serializable { private Long taskId; /** - * 协作方式(1审批 2转办 3委派 4会签 5票签 6加签 7减签) + * 协作方式(none无 transfer转办 delegate委派 add_signer加签 remove_signer减签) */ - private Integer cooperateType; + private String cooperateType; /** - * 协作方式(1审批 2转办 3委派 4会签 5票签 6加签 7减签) + * 协作方式展示名,由 cooperateType 翻译而来 */ private String cooperateTypeName; @@ -246,7 +246,7 @@ public class FlowHisTaskVo implements Serializable { /** * 设置协作方式,并通过协作方式获取名称 */ - public void setCooperateType(Integer cooperateType) { + public void setCooperateType(String cooperateType) { this.cooperateType = cooperateType; this.cooperateTypeName = CooperateType.getValueByKey(cooperateType); } diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/NodeExtVo.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/NodeExtVo.java index 5fb3380b4..55edd844e 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/NodeExtVo.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/NodeExtVo.java @@ -6,7 +6,6 @@ import java.io.Serial; import java.io.Serializable; import java.util.List; import java.util.Map; -import java.util.Set; /** * Node 扩展属性解析结果 VO @@ -28,13 +27,6 @@ public class NodeExtVo implements Serializable { */ private List buttonPermissions; - /** - * 抄送对象 ID 集合 - *

- * 根据扩展属性中 CopySettingEnum 类型的数据生成,存储需要抄送的对象 ID - */ - private Set copySettings; - /** * 自定义参数 Map *

diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/TestLeaveVo.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/TestLeaveVo.java index b66c25279..7d7a5e08c 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/TestLeaveVo.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/TestLeaveVo.java @@ -73,4 +73,9 @@ public class TestLeaveVo implements Serializable { @ExcelProperty(value = "状态") private String status; + /** + * 流程定义编码(已有流程实例时回显,供修改后重新提交沿用) + */ + private String flowCode; + } diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/handler/WorkflowEventPublisher.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/handler/WorkflowEventPublisher.java new file mode 100644 index 000000000..ba9c154ba --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/handler/WorkflowEventPublisher.java @@ -0,0 +1,33 @@ +package org.dromara.workflow.handler; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.warm.flow.core.service.event.EventMessage; +import org.dromara.warm.flow.core.service.event.EventPublisher; +import org.dromara.workflow.common.ConditionalOnEnable; +import org.springframework.stereotype.Component; + +/** + * 流程消息事件的宿主发布器 + *

+ * 引擎在 {@code MESSAGE_THROW} / {@code MESSAGE_END} 时经 {@code FlowEngine.eventPublisher()} + * (即容器里唯一的 {@link EventPublisher} Bean)把消息交出来。没有这个 Bean 时引擎只会告警并跳过, + * 消息事件看起来“配了但没生效”。 + *

+ * 当前实现只落日志,够演示与排查用;接真实中间件时在这里改成发 MQ / 调用外部接口, + * 注意本方法与流程状态更新在同一事务边界内,需要可靠投递请自行引入 Outbox。 + * + * @author may + */ +@ConditionalOnEnable +@Slf4j +@Component +public class WorkflowEventPublisher implements EventPublisher { + + @Override + public void publish(EventMessage message) { + log.info("收到流程消息事件: eventName={}, eventType={}, definitionId={}, instanceId={}, nodeCode={}, payload={}", + message.getEventName(), message.getEventType(), message.getDefinitionId(), message.getInstanceId(), + message.getNodeCode(), message.getPayload()); + } + +} diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/handler/WorkflowPermissionHandler.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/handler/WorkflowPermissionHandler.java deleted file mode 100644 index bebd4740c..000000000 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/handler/WorkflowPermissionHandler.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.dromara.workflow.handler; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.convert.Convert; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.dromara.common.core.utils.StreamUtils; -import org.dromara.common.core.utils.StringUtils; -import org.dromara.common.satoken.utils.LoginHelper; -import org.dromara.system.api.domain.UserDTO; -import org.dromara.warm.flow.core.dto.FlowParams; -import org.dromara.warm.flow.core.handler.PermissionHandler; -import org.dromara.workflow.common.ConditionalOnEnable; -import org.dromara.workflow.service.IFlwTaskAssigneeService; -import org.springframework.stereotype.Component; - -import java.util.Collections; -import java.util.List; - -/** - * 办理人权限处理器 - * - * @author AprilWind - */ -@ConditionalOnEnable -@RequiredArgsConstructor -@Component -@Slf4j -public class WorkflowPermissionHandler implements PermissionHandler { - - private final IFlwTaskAssigneeService flwTaskAssigneeService; - - /** - * 办理人权限标识,比如用户,角色,部门等,用于校验是否有权限办理任务 - * 后续在{@link FlowParams#getPermissionFlag} 中获取 - * 返回当前用户权限集合 - * - * @return 当前用户权限集合 - */ - @Override - public List permissions() { - return Collections.singletonList(LoginHelper.getUserIdStr()); - } - - /** - * 获取当前办理人 - * - * @return 当前办理人 - */ - @Override - public String getHandler() { - return LoginHelper.getUserIdStr(); - } - - /** - * 将预设办理人转换为实际用户 ID 列表。 - * - * @param permissions 预设权限标识列表 - * @return 用户 ID 列表 - */ - @Override - public List convertPermissions(List permissions) { - if (CollUtil.isEmpty(permissions)) { - return permissions; - } - String storageIds = CollUtil.join(permissions, StringUtils.SEPARATOR); - List users = flwTaskAssigneeService.fetchUsersByStorageIds(storageIds); - return StreamUtils.toList(users, userDTO -> Convert.toStr(userDTO.getUserId())); - } -} diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/listener/WorkflowGlobalListener.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/listener/WorkflowGlobalListener.java index ab958035e..d002be438 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/listener/WorkflowGlobalListener.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/listener/WorkflowGlobalListener.java @@ -1,7 +1,6 @@ package org.dromara.workflow.listener; import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.convert.Convert; import cn.hutool.core.lang.TypeReference; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ObjectUtil; @@ -9,19 +8,17 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.enums.BusinessStatusEnum; import org.dromara.common.core.utils.SpringUtils; -import org.dromara.common.core.utils.StreamUtils; import org.dromara.common.core.utils.StringUtils; -import org.dromara.system.api.UserService; import org.dromara.warm.flow.core.FlowEngine; -import org.dromara.warm.flow.core.dto.FlowParams; +import org.dromara.warm.flow.core.workflow.context.WorkflowContext; import org.dromara.warm.flow.core.entity.Definition; import org.dromara.warm.flow.core.entity.Instance; import org.dromara.warm.flow.core.entity.Task; -import org.dromara.warm.flow.core.listener.GlobalListener; -import org.dromara.warm.flow.core.listener.ListenerVariable; +import org.dromara.warm.flow.core.enums.TaskStatus; +import org.dromara.warm.flow.core.spi.GlobalListener; +import org.dromara.warm.flow.core.spi.ListenerVariable; import org.dromara.workflow.common.ConditionalOnEnable; import org.dromara.workflow.common.constant.FlowConstant; -import org.dromara.workflow.common.enums.TaskStatusEnum; import org.dromara.workflow.domain.bo.FlowCopyBo; import org.dromara.workflow.domain.vo.NodeExtVo; import org.dromara.workflow.event.WorkflowCopyEvent; @@ -37,7 +34,6 @@ import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; /** * 工作流全局监听器,处理任务流转中的扩展变量、消息和事件发布。 @@ -55,7 +51,6 @@ public class WorkflowGlobalListener implements GlobalListener { private final FlowProcessEventHandler flowProcessEventHandler; private final IFlwCommonService flwCommonService; private final IFlwNodeExtService nodeExtService; - private final UserService userService; /** * 任务创建回调,当前预留扩展。 @@ -81,20 +76,6 @@ public class WorkflowGlobalListener implements GlobalListener { variable = new HashMap<>(); } NodeExtVo nodeExt = nodeExtService.parseNodeExt(ext, variable); - Set copyList = nodeExt.getCopySettings(); - if (CollUtil.isNotEmpty(copyList)) { - List userIds = StreamUtils.toList(copyList, Convert::toLong); - Map nickNameMap = userService.selectUserNicksByIds(userIds); - List list = StreamUtils.toList(copyList, x -> { - FlowCopyBo bo = new FlowCopyBo(); - Long id = Convert.toLong(x); - bo.setUserId(id); - bo.setNickName(nickNameMap.getOrDefault(id, StringUtils.EMPTY)); - return bo; - }); - // 节点配置仅作为默认值,保留本次办理显式提交的抄送人或空名单。 - variable.putIfAbsent(FlowConstant.FLOW_COPY_LIST, list); - } if (CollUtil.isNotEmpty(nodeExt.getVariables())) { variable.putAll(nodeExt.getVariables()); } @@ -110,20 +91,20 @@ public class WorkflowGlobalListener implements GlobalListener { public void assignment(ListenerVariable listenerVariable) { Map variable = listenerVariable.getVariable(); List nextTasks = listenerVariable.getNextTasks(); - FlowParams flowParams = listenerVariable.getFlowParams(); + WorkflowContext workflowContext = listenerVariable.getContext(); Definition definition = listenerVariable.getDefinition(); Instance instance = listenerVariable.getInstance(); String applyNodeCode = flwCommonService.applyNodeCode(definition.getId()); - String hisStatus = flowParams != null ? flowParams.getHisStatus() : null; + String taskStatus = workflowContext != null ? workflowContext.getTaskStatus() : null; for (Task flowTask : nextTasks) { String nodeCode = flowTask.getNodeCode(); // 处理办理或退回时指定办理人的情况 - if (TaskStatusEnum.PASS.getStatus().equals(hisStatus)) { - processTaskPermission(variable, flowTask, hisStatus); - } else if (TaskStatusEnum.BACK.getStatus().equals(hisStatus)) { - processTaskPermission(variable, flowTask, hisStatus); + if (TaskStatus.PASS.getKey().equals(taskStatus)) { + processTaskPermission(variable, flowTask, taskStatus); + } else if (TaskStatus.BACK.getKey().equals(taskStatus)) { + processTaskPermission(variable, flowTask, taskStatus); } // 如果是申请节点,则把启动人添加到办理人 @@ -163,7 +144,7 @@ public class WorkflowGlobalListener implements GlobalListener { List userIdList = StringUtils.str2List(userIds, StringUtils.SEPARATOR, true, true); if (CollUtil.isNotEmpty(userIdList)) { flowTask.setPermissionList(userIdList); - if (TaskStatusEnum.PASS.getStatus().equals(taskStatus)) { + if (TaskStatus.PASS.getKey().equals(taskStatus)) { // 办理指定人变量只消费一次;驳回指定人变量需要保留给后续重复驳回。 variable.remove(nodeKey); FlowEngine.insService().removeVariables(flowTask.getInstanceId(), nodeKey); @@ -183,16 +164,16 @@ public class WorkflowGlobalListener implements GlobalListener { Task task = listenerVariable.getTask(); List nextTasks = listenerVariable.getNextTasks(); Map params = new HashMap<>(); - FlowParams flowParams = listenerVariable.getFlowParams(); + WorkflowContext workflowContext = listenerVariable.getContext(); Map variable = new HashMap<>(); - if (ObjectUtil.isNotNull(flowParams)) { + if (ObjectUtil.isNotNull(workflowContext)) { // 历史任务扩展(通常为附件) - params.put("hisTaskExt", flowParams.getHisTaskExt()); + params.put("hisTaskExt", workflowContext.getExt()); // 办理人 - params.put("handler", flowParams.getHandler()); + params.put("handler", workflowContext.getHandler()); // 办理意见 - params.put("message", flowParams.getMessage()); - variable = flowParams.getVariable(); + params.put("message", workflowContext.getMessage()); + variable = workflowContext.getVariables(); } //申请人提交事件 Boolean submit = MapUtil.getBool(variable, FlowConstant.SUBMIT); @@ -224,11 +205,11 @@ public class WorkflowGlobalListener implements GlobalListener { flowProcessEventHandler.processTaskHandler(definition.getFlowCode(), instance, nextTask, params); } } - if (ObjectUtil.isNull(flowParams)) { + if (ObjectUtil.isNull(workflowContext)) { return; } // 只有办理或者退回的时候才执行消息通知和抄送 - if (!TaskStatusEnum.isPassOrBack(flowParams.getHisStatus())) { + if (!isPassOrBack(workflowContext.getTaskStatus())) { return; } if (ObjectUtil.isNull(variable)) { @@ -246,7 +227,7 @@ public class WorkflowGlobalListener implements GlobalListener { }); String notice = MapUtil.getStr(variable, FlowConstant.MESSAGE_NOTICE); // 退回到申请人时只保留“已退回”结果消息,避免再追加一条“新的待办”形成重复提醒。 - if (shouldSendTaskMessage(flowParams, definition, nextTasks)) { + if (shouldSendTaskMessage(workflowContext, definition, nextTasks)) { SpringUtils.context().publishEvent(new WorkflowTaskMessageEvent(definition.getFlowName(), instance.getId(), messageType, notice)); } } @@ -261,13 +242,13 @@ public class WorkflowGlobalListener implements GlobalListener { /** * 判断是否需要发送后续待办消息。 * - * @param flowParams 流程参数 + * @param workflowContext 流程执行上下文 * @param definition 流程定义 * @param nextTasks 后续任务列表 * @return 是否发送待办消息 */ - private boolean shouldSendTaskMessage(FlowParams flowParams, Definition definition, List nextTasks) { - if (flowParams == null || !TaskStatusEnum.BACK.getStatus().equals(flowParams.getHisStatus())) { + private boolean shouldSendTaskMessage(WorkflowContext workflowContext, Definition definition, List nextTasks) { + if (workflowContext == null || !TaskStatus.BACK.getKey().equals(workflowContext.getTaskStatus())) { return true; } if (CollUtil.isEmpty(nextTasks) || nextTasks.size() != 1) { @@ -302,6 +283,16 @@ public class WorkflowGlobalListener implements GlobalListener { SpringUtils.context().publishEvent(new WorkflowResultMessageEvent(definition.getFlowName(), status, instance.getCreateBy(), messageType)); } + /** + * 任务收口是否为「通过」或「退回」,只有这两种结果才触发消息通知与抄送。 + * + * @param taskStatus 任务状态编码 + * @return 是否为通过或退回 + */ + private boolean isPassOrBack(String taskStatus) { + return TaskStatus.PASS.getKey().equals(taskStatus) || TaskStatus.BACK.getKey().equals(taskStatus); + } + /** * 根据流程实例确定最终状态 * diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/complete/CompleteAutoPassComponent.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/complete/CompleteAutoPassComponent.java deleted file mode 100644 index cb21570f0..000000000 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/complete/CompleteAutoPassComponent.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.dromara.workflow.liteflow.complete; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.ObjectUtil; -import com.yomahub.liteflow.annotation.LiteflowComponent; -import com.yomahub.liteflow.core.NodeComponent; -import lombok.RequiredArgsConstructor; -import org.dromara.common.core.utils.StreamUtils; -import org.dromara.common.core.utils.StringUtils; -import org.dromara.common.mybatis.core.query.QueryBuilder; -import org.dromara.common.satoken.utils.LoginHelper; -import org.dromara.warm.flow.core.FlowEngine; -import org.dromara.warm.flow.core.dto.FlowParams; -import org.dromara.warm.flow.core.entity.User; -import org.dromara.warm.flow.core.service.TaskService; -import org.dromara.warm.flow.orm.entity.FlowTask; -import org.dromara.warm.flow.orm.mapper.FlowTaskMapper; -import org.dromara.workflow.common.ConditionalOnEnable; -import org.dromara.workflow.domain.context.CompleteTaskContext; - -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import static org.dromara.workflow.common.constant.FlowConstant.*; - -/** - * 执行当前处理人的后续自动审批。 - *

- * 仅当流程实例开启自动审批时生效,用于连续跳过同一登录人负责的后续待办任务。 - * - * @author may - */ -@ConditionalOnEnable -@RequiredArgsConstructor -@LiteflowComponent("completeAutoPass") -public class CompleteAutoPassComponent extends NodeComponent { - - private final TaskService taskService; - private final FlowTaskMapper flowTaskMapper; - - @Override - public void process() { - CompleteTaskContext context = getContextBean(CompleteTaskContext.class); - FlowTask flowTask = context.getFlowTask(); - autoPass(flowTask.getId(), context.getFlowParams(), flowTask.getInstanceId()); - } - - /** - * 递归跳过当前处理人的后续任务。 - * - * @param taskId 本轮已处理任务 id,用于避免重复跳过同一任务 - * @param flowParams 当前办理参数,会在自动审批时覆盖为自动审批消息和空抄送变量 - * @param instanceId 流程实例 id - */ - private void autoPass(Long taskId, FlowParams flowParams, Long instanceId) { - List flowTaskList = selectByInstId(instanceId); - if (CollUtil.isEmpty(flowTaskList)) { - return; - } - List userList = FlowEngine.userService() - .getByAssociateds(StreamUtils.toList(flowTaskList, FlowTask::getId)); - if (CollUtil.isEmpty(userList)) { - return; - } - for (FlowTask task : flowTaskList) { - if (task.getId().equals(taskId)) { - continue; - } - // 自动审批只处理当前登录人仍是办理人的后续任务,避免替其他候选人或并行分支误审批。 - List users = StreamUtils.filter(userList, - e -> ObjectUtil.equals(task.getId(), e.getAssociated()) && ObjectUtil.equal(e.getProcessedBy(), LoginHelper.getUserIdStr())); - if (CollUtil.isEmpty(users)) { - continue; - } - // 自动审批不继承人工办理时的抄送和消息通知,避免重复发送业务消息。 - flowParams - .message("流程引擎自动审批!") - .variable(Map.of( - SUBMIT, false, - FLOW_COPY_LIST, Collections.emptyList(), - MESSAGE_NOTICE, StringUtils.EMPTY)); - taskService.skip(task.getId(), flowParams); - // 跳过后可能生成新的后续任务,继续扫描直到当前登录人不再有可自动办理任务。 - autoPass(task.getId(), flowParams, instanceId); - } - } - - private List selectByInstId(Long instanceId) { - return flowTaskMapper.selectList(QueryBuilder.lambda(FlowTask.class) - .eq(FlowTask::getInstanceId, instanceId) - .build()); - } - -} diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/complete/CompleteExecuteComponent.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/complete/CompleteExecuteComponent.java index a6cd00dd3..84e535880 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/complete/CompleteExecuteComponent.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/complete/CompleteExecuteComponent.java @@ -4,12 +4,11 @@ import cn.hutool.core.convert.Convert; import com.yomahub.liteflow.annotation.LiteflowComponent; import com.yomahub.liteflow.core.NodeComponent; import lombok.RequiredArgsConstructor; -import org.dromara.common.core.enums.BusinessStatusEnum; -import org.dromara.warm.flow.core.dto.FlowParams; -import org.dromara.warm.flow.core.enums.SkipType; -import org.dromara.warm.flow.core.service.TaskService; +import org.dromara.warm.flow.core.FlowEngine; +import org.dromara.warm.flow.core.enums.TaskStatus; +import org.dromara.warm.flow.core.workflow.command.CompleteCommand; +import org.dromara.warm.flow.core.workflow.context.OperatorContext; import org.dromara.workflow.common.ConditionalOnEnable; -import org.dromara.workflow.common.enums.TaskStatusEnum; import org.dromara.workflow.domain.bo.CompleteTaskBo; import org.dromara.workflow.domain.context.CompleteTaskContext; @@ -27,28 +26,23 @@ import static org.dromara.workflow.common.constant.FlowConstant.*; @LiteflowComponent("completeExecute") public class CompleteExecuteComponent extends NodeComponent { - private final TaskService taskService; - @Override public void process() { CompleteTaskContext context = getContextBean(CompleteTaskContext.class); CompleteTaskBo completeTaskBo = context.getCompleteTaskBo(); Map variables = completeTaskBo.getVariables(); - FlowParams flowParams = FlowParams.build() - .handler(completeTaskBo.getHandler()) - .variable(variables) - .ignore(Convert.toBool(variables.getOrDefault(VAR_IGNORE, false))) - .ignoreDepute(Convert.toBool(variables.getOrDefault(VAR_IGNORE_DEPUTE, false))) - .ignoreCooperate(Convert.toBool(variables.getOrDefault(VAR_IGNORE_COOPERATE, false))) - .skipType(SkipType.PASS.getKey()) - .message(completeTaskBo.getMessage()) - .flowStatus(BusinessStatusEnum.WAITING.getStatus()) - .hisStatus(TaskStatusEnum.PASS.getStatus()) - .hisTaskExt(completeTaskBo.getFileId()); - - context.setFlowParams(flowParams); - context.setAutoPass(Convert.toBool(context.getInstance().getVariableMap().getOrDefault(AUTO_PASS, false))); - taskService.skip(context.getFlowTask().getId(), flowParams); + CompleteCommand command = new CompleteCommand(); + OperatorContext operator = new OperatorContext(completeTaskBo.getHandler()); + operator.setIgnore(Convert.toBool(variables.getOrDefault(VAR_IGNORE, false))); + command.setOperator(operator); + command.setTaskId(context.getFlowTask().getId()); + command.setVariables(variables); + command.setMessage(completeTaskBo.getMessage()); + command.setExt(completeTaskBo.getFileId()); + // 全局监听器用 context.taskStatus 判断「通过/退回」,才决定要不要抄送与发通知; + // 引擎不会把解析后的状态回填进调用上下文,办理路径不显式声明 PASS 就会静默跳过抄送。 + command.setTaskStatus(TaskStatus.PASS.getKey()); + FlowEngine.workflow().complete(command); } } diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/complete/CompleteNeedAutoPassComponent.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/complete/CompleteNeedAutoPassComponent.java deleted file mode 100644 index d74646c7d..000000000 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/complete/CompleteNeedAutoPassComponent.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.dromara.workflow.liteflow.complete; - -import com.yomahub.liteflow.annotation.LiteflowComponent; -import com.yomahub.liteflow.core.NodeBooleanComponent; -import org.dromara.workflow.common.ConditionalOnEnable; -import org.dromara.workflow.domain.context.CompleteTaskContext; - -/** - * 判断办理任务后是否进入自动审批分支。 - * - * @author may - */ -@ConditionalOnEnable -@LiteflowComponent("completeNeedAutoPass") -public class CompleteNeedAutoPassComponent extends NodeBooleanComponent { - - @Override - public boolean processBoolean() { - CompleteTaskContext context = getContextBean(CompleteTaskContext.class); - return Boolean.TRUE.equals(context.getAutoPass()); - } - -} diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/complete/CompletePrepareComponent.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/complete/CompletePrepareComponent.java index ad3bb56e2..54d7a5edb 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/complete/CompletePrepareComponent.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/complete/CompletePrepareComponent.java @@ -9,11 +9,11 @@ import org.dromara.common.core.enums.BusinessStatusEnum; import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.utils.StringUtils; import org.dromara.warm.flow.core.entity.Instance; +import org.dromara.warm.flow.core.enums.TaskStatus; import org.dromara.warm.flow.core.service.InsService; import org.dromara.warm.flow.orm.entity.FlowTask; import org.dromara.warm.flow.orm.mapper.FlowTaskMapper; import org.dromara.workflow.common.ConditionalOnEnable; -import org.dromara.workflow.common.enums.TaskStatusEnum; import org.dromara.workflow.domain.bo.CompleteTaskBo; import org.dromara.workflow.domain.context.CompleteTaskContext; @@ -86,12 +86,12 @@ public class CompletePrepareComponent extends NodeComponent { hashSet.addAll(popUserIds); hashSet.addAll(variableUserIds); // Warm-Flow 会按“状态:变量名”读取办理人,正向和退回都写入同一批候选人。 - map.put(TaskStatusEnum.PASS.getStatus() + StringUtils.COLON + entry.getKey(), StringUtils.joinComma(hashSet)); - map.put(TaskStatusEnum.BACK.getStatus() + StringUtils.COLON + entry.getKey(), StringUtils.joinComma(hashSet)); + map.put(TaskStatus.PASS.getKey() + StringUtils.COLON + entry.getKey(), StringUtils.joinComma(hashSet)); + map.put(TaskStatus.BACK.getKey() + StringUtils.COLON + entry.getKey(), StringUtils.joinComma(hashSet)); } } else { - map.put(TaskStatusEnum.PASS.getStatus() + StringUtils.COLON + entry.getKey(), entry.getValue()); - map.put(TaskStatusEnum.BACK.getStatus() + StringUtils.COLON + entry.getKey(), entry.getValue()); + map.put(TaskStatus.PASS.getKey() + StringUtils.COLON + entry.getKey(), entry.getValue()); + map.put(TaskStatus.BACK.getKey() + StringUtils.COLON + entry.getKey(), entry.getValue()); } } return map; diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/instance/InstanceDeleteExecuteComponent.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/instance/InstanceDeleteExecuteComponent.java index 8e27a47e1..00ec10228 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/instance/InstanceDeleteExecuteComponent.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/instance/InstanceDeleteExecuteComponent.java @@ -7,13 +7,13 @@ import lombok.RequiredArgsConstructor; import org.dromara.common.core.utils.StreamUtils; import org.dromara.common.mybatis.core.query.QueryBuilder; import org.dromara.warm.flow.core.FlowEngine; +import org.dromara.warm.flow.core.enums.TaskStatus; import org.dromara.warm.flow.core.service.InsService; import org.dromara.warm.flow.orm.entity.FlowHisTask; import org.dromara.warm.flow.orm.entity.FlowTask; import org.dromara.warm.flow.orm.mapper.FlowHisTaskMapper; import org.dromara.warm.flow.orm.mapper.FlowTaskMapper; import org.dromara.workflow.common.ConditionalOnEnable; -import org.dromara.workflow.common.enums.TaskStatusEnum; import org.dromara.workflow.domain.context.InstanceDeleteContext; import java.util.List; @@ -56,23 +56,27 @@ public class InstanceDeleteExecuteComponent extends NodeComponent { /** * 删除流程实例产生的抄送人员关联。 * + *

抄送人挂在抄送历史主键上(与引擎 {@code CopyAdvancer} 一致),不是待办主键。 + * 只有本组件的历史分支需要显式清理——非历史分支走 {@code insService.remove}, + * 引擎级联里已按历史主键清过一遍。

+ * * @param instanceIds 流程实例 ID 集合 */ private void deleteCopyUsers(List instanceIds) { List copyTasks = flowHisTaskMapper.selectList(QueryBuilder.lambda(FlowHisTask.class) .in(FlowHisTask::getInstanceId, instanceIds) - .eq(FlowHisTask::getFlowStatus, TaskStatusEnum.COPY.getStatus()) + .eq(FlowHisTask::getFlowStatus, TaskStatus.COPY.getKey()) .build()); if (CollUtil.isEmpty(copyTasks)) { return; } - List copyTaskIds = copyTasks.stream() - .map(FlowHisTask::getTaskId) + List copyHisIds = copyTasks.stream() + .map(FlowHisTask::getId) .filter(Objects::nonNull) .distinct() .toList(); - if (CollUtil.isNotEmpty(copyTaskIds)) { - FlowEngine.userService().deleteByTaskIds(copyTaskIds); + if (CollUtil.isNotEmpty(copyHisIds)) { + FlowEngine.userService().deleteByTaskIds(copyHisIds); } } diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/operation/TaskOpExecuteComponent.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/operation/TaskOpExecuteComponent.java index dc2b77825..a2ace850c 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/operation/TaskOpExecuteComponent.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/operation/TaskOpExecuteComponent.java @@ -3,9 +3,13 @@ package org.dromara.workflow.liteflow.operation; import com.yomahub.liteflow.annotation.LiteflowComponent; import com.yomahub.liteflow.core.NodeComponent; import lombok.RequiredArgsConstructor; -import org.dromara.warm.flow.core.service.TaskService; +import org.dromara.warm.flow.core.FlowEngine; +import org.dromara.warm.flow.core.workflow.command.AddSignerCommand; +import org.dromara.warm.flow.core.workflow.command.DelegateCommand; +import org.dromara.warm.flow.core.workflow.command.RemoveSignerCommand; +import org.dromara.warm.flow.core.workflow.command.TransferCommand; +import org.dromara.warm.flow.core.workflow.context.OperatorContext; import org.dromara.workflow.common.ConditionalOnEnable; -import org.dromara.workflow.common.enums.TaskStatusEnum; import org.dromara.workflow.domain.context.TaskOperationContext; /** @@ -18,29 +22,45 @@ import org.dromara.workflow.domain.context.TaskOperationContext; @LiteflowComponent("taskOpExecute") public class TaskOpExecuteComponent extends NodeComponent { - private final TaskService taskService; - @Override public void process() { TaskOperationContext context = getContextBean(TaskOperationContext.class); Long taskId = context.getTaskOperationBo().getTaskId(); boolean result = false; + OperatorContext operator = context.getOperator(); + String message = context.getMessage(); switch (context.getOperation()) { case DELEGATE_TASK -> { - context.getFlowParams().hisStatus(TaskStatusEnum.DEPUTE.getStatus()); - result = taskService.depute(taskId, context.getFlowParams()); + DelegateCommand command = new DelegateCommand(); + command.setOperator(operator); + command.setTaskId(taskId); + command.setTargetHandler(context.getTaskOperationBo().getUserId()); + command.setMessage(message); + result = FlowEngine.workflow().delegate(command).isSuccess(); } case TRANSFER_TASK -> { - context.getFlowParams().hisStatus(TaskStatusEnum.TRANSFER.getStatus()); - result = taskService.transfer(taskId, context.getFlowParams()); + TransferCommand command = new TransferCommand(); + command.setOperator(operator); + command.setTaskId(taskId); + command.setTargetHandler(context.getTaskOperationBo().getUserId()); + command.setMessage(message); + result = FlowEngine.workflow().transfer(command).isSuccess(); } case ADD_SIGNATURE -> { - context.getFlowParams().hisStatus(TaskStatusEnum.SIGN.getStatus()); - result = taskService.addSignature(taskId, context.getFlowParams()); + AddSignerCommand command = new AddSignerCommand(); + command.setOperator(operator); + command.setTaskId(taskId); + command.setTargetHandlers(context.getTaskOperationBo().getUserIds()); + command.setMessage(message); + result = FlowEngine.workflow().addSigner(command).isSuccess(); } case REDUCTION_SIGNATURE -> { - context.getFlowParams().hisStatus(TaskStatusEnum.SIGN_OFF.getStatus()); - result = taskService.reductionSignature(taskId, context.getFlowParams()); + RemoveSignerCommand command = new RemoveSignerCommand(); + command.setOperator(operator); + command.setTaskId(taskId); + command.setTargetHandlers(context.getTaskOperationBo().getUserIds()); + command.setMessage(message); + result = FlowEngine.workflow().removeSigner(command).isSuccess(); } } context.setResult(result); diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/operation/TaskOpPrepareComponent.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/operation/TaskOpPrepareComponent.java index 4cfc0d6a2..aca3c6871 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/operation/TaskOpPrepareComponent.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/operation/TaskOpPrepareComponent.java @@ -8,14 +8,12 @@ import org.dromara.common.core.utils.ValidatorUtils; import org.dromara.common.core.validate.AddGroup; import org.dromara.common.core.validate.EditGroup; import org.dromara.common.satoken.utils.LoginHelper; -import org.dromara.warm.flow.core.dto.FlowParams; +import org.dromara.warm.flow.core.workflow.context.OperatorContext; import org.dromara.workflow.common.ConditionalOnEnable; import org.dromara.workflow.common.enums.TaskOperationEnum; import org.dromara.workflow.domain.bo.TaskOperationBo; import org.dromara.workflow.domain.context.TaskOperationContext; -import java.util.Collections; - /** * 准备任务操作参数。 * @@ -42,16 +40,10 @@ public class TaskOpPrepareComponent extends NodeComponent { case ADD_SIGNATURE, REDUCTION_SIGNATURE -> ValidatorUtils.validate(bo, EditGroup.class); } - FlowParams flowParams = FlowParams.build().message(bo.getMessage()); - if (LoginHelper.isSuperAdmin()) { - flowParams.ignore(true); - } - switch (op) { - case DELEGATE_TASK, TRANSFER_TASK -> flowParams.addHandlers(Collections.singletonList(bo.getUserId())); - case ADD_SIGNATURE -> flowParams.addHandlers(bo.getUserIds()); - case REDUCTION_SIGNATURE -> flowParams.reductionHandlers(bo.getUserIds()); - } - context.setFlowParams(flowParams); + OperatorContext operator = new OperatorContext(LoginHelper.getUserIdStr()); + operator.setIgnore(LoginHelper.isSuperAdmin()); + context.setOperator(operator); + context.setMessage(bo.getMessage()); } } diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/start/StartExecuteComponent.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/start/StartExecuteComponent.java index eacbcee8b..f96273630 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/start/StartExecuteComponent.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/start/StartExecuteComponent.java @@ -6,11 +6,14 @@ import com.yomahub.liteflow.core.NodeComponent; import lombok.RequiredArgsConstructor; import org.dromara.common.core.enums.BusinessStatusEnum; import org.dromara.common.core.exception.ServiceException; -import org.dromara.warm.flow.core.dto.FlowParams; +import org.dromara.warm.flow.core.FlowEngine; import org.dromara.warm.flow.core.entity.Instance; import org.dromara.warm.flow.core.entity.Task; -import org.dromara.warm.flow.core.service.InsService; +import org.dromara.warm.flow.core.enums.TaskStatus; import org.dromara.warm.flow.core.service.TaskService; +import org.dromara.warm.flow.core.workflow.command.StartCommand; +import org.dromara.warm.flow.core.workflow.context.OperatorContext; +import org.dromara.warm.flow.core.workflow.result.WorkflowResult; import org.dromara.warm.flow.orm.entity.FlowTask; import org.dromara.workflow.api.domain.StartProcessReturnDTO; import org.dromara.workflow.common.ConditionalOnEnable; @@ -30,19 +33,21 @@ import java.util.List; @LiteflowComponent("startExecute") public class StartExecuteComponent extends NodeComponent { - private final InsService insService; private final TaskService taskService; private final FlwInstanceBizExtMapper flwInstanceBizExtMapper; @Override public void process() { StartProcessContext context = getContextBean(StartProcessContext.class); - FlowParams flowParams = FlowParams.build() - .handler(context.getStartProcessBo().getHandler()) - .flowCode(context.getStartProcessBo().getFlowCode()) - .variable(context.getVariables()) - .flowStatus(BusinessStatusEnum.DRAFT.getStatus()); - Instance instance = insService.start(context.getBusinessId(), flowParams); + StartCommand command = new StartCommand(); + command.setBusinessId(context.getBusinessId()); + command.setFlowCode(context.getStartProcessBo().getFlowCode()); + command.setOperator(new OperatorContext(context.getStartProcessBo().getHandler())); + command.setVariables(context.getVariables()); + command.setFlowStatus(BusinessStatusEnum.DRAFT.getStatus()); + command.setTaskStatus(TaskStatus.PASS.getKey()); + WorkflowResult result = FlowEngine.workflow().start(command); + Instance instance = FlowEngine.insService().getById(result.getInstanceId()); context.setInstance(instance); saveBizExt(context, instance); diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/start/StartPrepareInstanceComponent.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/start/StartPrepareInstanceComponent.java index fc3cfa753..da642e965 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/start/StartPrepareInstanceComponent.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/liteflow/start/StartPrepareInstanceComponent.java @@ -1,12 +1,10 @@ package org.dromara.workflow.liteflow.start; import cn.hutool.core.convert.Convert; -import cn.hutool.core.lang.Dict; import cn.hutool.core.util.ObjectUtil; import com.yomahub.liteflow.annotation.LiteflowComponent; import com.yomahub.liteflow.core.NodeComponent; import org.dromara.common.core.exception.ServiceException; -import org.dromara.common.json.utils.JsonUtils; import org.dromara.warm.flow.core.FlowEngine; import org.dromara.warm.flow.core.entity.Definition; import org.dromara.workflow.common.ConditionalOnEnable; @@ -36,9 +34,6 @@ public class StartPrepareInstanceComponent extends NodeComponent { context.setDefinition(definition); Map variables = context.getVariables(); - Dict dict = JsonUtils.parseMap(definition.getExt()); - boolean autoPass = !ObjectUtil.isNull(dict) && dict.getBool(AUTO_PASS); - variables.put(AUTO_PASS, autoPass); variables.put(BUSINESS_CODE, generateBusinessCode(context.getBizExt())); } diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/FlwHisTaskMapper.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/FlwHisTaskMapper.java index 5cc2e8c9d..8b9329a6b 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/FlwHisTaskMapper.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/FlwHisTaskMapper.java @@ -5,10 +5,10 @@ import com.github.yulichang.base.MPJBaseMapper; import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; import org.dromara.common.mybatis.core.query.QueryBuilder; import org.dromara.warm.flow.core.enums.NodeType; +import org.dromara.warm.flow.core.enums.TaskStatus; import org.dromara.warm.flow.orm.entity.FlowDefinition; import org.dromara.warm.flow.orm.entity.FlowHisTask; import org.dromara.warm.flow.orm.entity.FlowInstance; -import org.dromara.workflow.common.enums.TaskStatusEnum; import org.dromara.workflow.domain.FlowInstanceBizExt; import org.dromara.workflow.domain.bo.FlowTaskBo; import org.dromara.workflow.domain.vo.FlowHisTaskVo; @@ -51,7 +51,7 @@ public interface FlwHisTaskMapper extends BaseMapperPlus, MPJ .select("d", FlowDefinition::getFlowName, FlowDefinition::getFlowCode, FlowDefinition::getCategory, FlowDefinition::getVersion) .select("biz", FlowInstanceBizExt::getBusinessCode, FlowInstanceBizExt::getBusinessTitle) - .leftJoin(FlowHisTask.class, "b", FlowHisTask::getTaskId, FlowUser::getAssociated) + .leftJoin(FlowHisTask.class, "b", FlowHisTask::getId, FlowUser::getAssociated) .leftJoin(FlowInstance.class, "c", FlowInstance::getId, FlowHisTask::getInstanceId) .leftJoin(FlowDefinition.class, "d", FlowDefinition::getId, FlowInstance::getDefinitionId) .leftJoin(FlowInstanceBizExt.class, "biz", FlowInstanceBizExt::getInstanceId, FlowInstance::getId) diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IFlwEventWaitService.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IFlwEventWaitService.java new file mode 100644 index 000000000..772d023a9 --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IFlwEventWaitService.java @@ -0,0 +1,60 @@ +package org.dromara.workflow.service; + +import org.dromara.common.core.domain.PageResult; +import org.dromara.workflow.domain.bo.FlowEventWaitBo; +import org.dromara.workflow.domain.bo.FlowTimerTriggerBo; +import org.dromara.workflow.domain.vo.FlowEventWaitVo; + +import java.util.Map; + +/** + * 定时任务(事件等待记录)服务接口。 + *

数据来自引擎 flow_event_wait,提供查询、状态统计、已死记录复活与定时启动任务的手动补发。 + * 手动补发只创建一次实例,不读取也不推进等待记录,定时任务的下次触发时间不受影响。

+ * + * @author may + * @since 2026/10/21 + */ +public interface IFlwEventWaitService { + + /** + * 分页查询事件等待记录,默认只查定时类事件、按下次触发时间升序。 + * + * @param bo 查询条件 + * @return 等待记录分页数据 + */ + PageResult queryPageList(FlowEventWaitBo bo); + + /** + * 按同一组筛选条件统计各状态等待记录数量。 + * + * @param bo 查询条件(其中状态条件被逐一分组覆盖) + * @return 各状态计数,键为 waiting / retry / running / dead / cancelled + */ + Map summary(FlowEventWaitBo bo); + + /** + * 查询单条等待记录详情。 + * + * @param id 事件等待记录主键 + * @return 等待记录详情,不存在时返回 {@code null} + */ + FlowEventWaitVo queryById(Long id); + + /** + * 手动补发一次定时启动任务(运维应急)。 + *

实例发起人记为当前登录用户;等待记录保持不变,「每天 9 点的任务今天提前补发一次」 + * 不会吞掉当天 9 点的正常触发。

+ * + * @param bo 补发参数(等待记录主键、可选业务主键与变量) + * @return 新建流程实例主键 + */ + Long manualTrigger(FlowTimerTriggerBo bo); + + /** + * 复活已死的事件等待记录,状态与实例有效性由引擎校验。 + * + * @param id 事件等待记录主键 + */ + void revive(Long id); +} diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IFlwInstanceService.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IFlwInstanceService.java index 38b50e08e..a2627b932 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IFlwInstanceService.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IFlwInstanceService.java @@ -119,6 +119,15 @@ public interface IFlwInstanceService { */ Map flowHisTaskList(String businessId); + /** + * 按实例 ID、业务 ID 查询流程图及历史办理记录;同时提供时须指向同一实例。 + * + * @param instanceId 流程实例 ID,可为空 + * @param businessId 业务 ID,可为空 + * @return 流程图与办理记录 + */ + Map flowHisTaskList(Long instanceId, String businessId); + /** * 按照实例id更新状态 * diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IFlwTaskAssigneeService.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IFlwTaskAssigneeService.java deleted file mode 100644 index d427b546d..000000000 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IFlwTaskAssigneeService.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.dromara.workflow.service; - -import org.dromara.system.api.domain.UserDTO; - -import java.util.List; - -/** - * 流程设计器-获取办理人 - * - * @author AprilWind - */ -public interface IFlwTaskAssigneeService { - - /** - * 批量解析多个存储标识符(storageIds),按类型分类并合并查询用户列表 - * 输入格式支持多个以逗号分隔的标识(如 "user:123,role:456,789") - * 会自动去重返回结果,非法格式的标识将被忽略 - * - * @param storageIds 多个存储标识符字符串(逗号分隔) - * @return 合并后的用户列表,去重后返回,非法格式的标识将被跳过 - */ - List fetchUsersByStorageIds(String storageIds); - -} diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwChartExtServiceImpl.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwChartExtServiceImpl.java index 7c9cd1747..2803388e3 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwChartExtServiceImpl.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwChartExtServiceImpl.java @@ -20,10 +20,15 @@ import org.dromara.warm.flow.core.dto.PromptContent; import org.dromara.warm.flow.core.enums.NodeType; import org.dromara.warm.flow.core.utils.MapUtil; import org.dromara.warm.flow.orm.entity.FlowHisTask; +import org.dromara.warm.flow.orm.entity.FlowTask; +import org.dromara.warm.flow.orm.entity.FlowUser; import org.dromara.warm.flow.orm.mapper.FlowHisTaskMapper; +import org.dromara.warm.flow.orm.mapper.FlowTaskMapper; +import org.dromara.warm.flow.orm.mapper.FlowUserMapper; import org.dromara.warm.flow.ui.service.ChartExtService; import org.dromara.workflow.common.ConditionalOnEnable; import org.dromara.workflow.common.constant.FlowConstant; +import org.dromara.workflow.common.enums.TaskAssigneeType; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -45,6 +50,8 @@ public class FlwChartExtServiceImpl implements ChartExtService { private final UserService userService; private final DeptService deptService; private final FlowHisTaskMapper flowHisTaskMapper; + private final FlowTaskMapper flowTaskMapper; + private final FlowUserMapper flowUserMapper; private final DictService dictService; @Value("${warm-flow.node-tooltip:true}") private boolean nodeTooltip; @@ -61,45 +68,80 @@ public class FlwChartExtServiceImpl implements ChartExtService { return; } - // 根据流程实例ID查询所有相关的历史任务列表 - List flowHisTasks = this.getHisTaskGroupedByNode(defJson.getInstance().getId()); - if (CollUtil.isEmpty(flowHisTasks)) { + Long instanceId = defJson.getInstance().getId(); + List flowHisTasks = this.getHisTaskGroupedByNode(instanceId); + List activeTasks = flowTaskMapper.selectList(Wrappers.lambdaQuery() + .eq(FlowTask::getInstanceId, instanceId) + .eq(FlowTask::getNodeType, NodeType.BETWEEN.getKey())); + if (CollUtil.isEmpty(flowHisTasks) && CollUtil.isEmpty(activeTasks)) { return; } - // 按节点编号(nodeCode)对历史任务进行分组 - Map> groupedByNode = StreamUtils.groupByKey(flowHisTasks, FlowHisTask::getNodeCode); + Map> groupedByNode = CollUtil.isEmpty(flowHisTasks) + ? Collections.emptyMap() : StreamUtils.groupByKey(flowHisTasks, FlowHisTask::getNodeCode); + Map taskNodeCodes = activeTasks.stream() + .collect(Collectors.toMap(FlowTask::getId, FlowTask::getNodeCode)); + List activeUsers = taskNodeCodes.isEmpty() ? Collections.emptyList() + : flowUserMapper.selectList(Wrappers.lambdaQuery() + .in(FlowUser::getAssociated, taskNodeCodes.keySet()) + .in(FlowUser::getType, List.of(TaskAssigneeType.APPROVER.getCode(), + TaskAssigneeType.TRANSFER.getCode(), TaskAssigneeType.DELEGATE.getCode()))); + Map> pendingUsersByNode = new HashMap<>(); + for (FlowUser activeUser : activeUsers) { + Long userId = Convert.toLong(activeUser.getProcessedBy(), null); + String nodeCode = taskNodeCodes.get(activeUser.getAssociated()); + if (userId != null && nodeCode != null) { + pendingUsersByNode.computeIfAbsent(nodeCode, key -> new LinkedHashSet<>()).add(userId); + } + } - // 批量查询所有审批人的用户信息 - List userDTOList = userService.selectListByIds(StreamUtils.toSet(flowHisTasks, e -> Convert.toLong(e.getApprover()))); + Set userIds = new HashSet<>(); + flowHisTasks.forEach(task -> { + Long approverId = Convert.toLong(task.getApprover(), null); + if (approverId != null) { + userIds.add(approverId); + } + }); + pendingUsersByNode.values().forEach(userIds::addAll); + if (userIds.isEmpty()) { + return; + } + List userDTOList = userService.selectListByIds(userIds); // 将查询到的用户列表转换为以用户ID为key的映射 Map userMap = StreamUtils.toIdentityMap(userDTOList, UserDTO::getUserId); Set deptIds = StreamUtils.toSet(userDTOList, UserDTO::getDeptId); - Map deptNameMap = deptService.selectDeptNamesByIds(deptIds); + deptIds.remove(null); + Map deptNameMap = deptIds.isEmpty() ? Collections.emptyMap() + : deptService.selectDeptNamesByIds(deptIds); - Map dictType = dictService.getAllDictByDictType(FlowConstant.WF_TASK_STATUS); + Map dictType = flowHisTasks.isEmpty() ? Collections.emptyMap() + : dictService.getAllDictByDictType(FlowConstant.WF_TASK_STATUS); for (NodeJson nodeJson : defJson.getNodeList()) { List taskList = groupedByNode.get(nodeJson.getNodeCode()); - if (CollUtil.isEmpty(taskList)) { - continue; + if (CollUtil.isNotEmpty(taskList)) { + List latestPerApprover = taskList.stream() + .filter(task -> StringUtils.isNotBlank(task.getApprover())) + .collect(Collectors.collectingAndThen( + Collectors.toMap( + FlowHisTask::getApprover, + Function.identity(), + (oldTask, newTask) -> newTask.getUpdateTime().after(oldTask.getUpdateTime()) ? newTask : oldTask, + LinkedHashMap::new + ), + map -> new ArrayList<>(map.values()) + )); + this.processNodeExtInfo(nodeJson, latestPerApprover, userMap, deptNameMap, dictType); + } + for (Long userId : pendingUsersByNode.getOrDefault(nodeJson.getNodeCode(), Collections.emptySet())) { + UserDTO user = userMap.get(userId); + if (user != null) { + addUserInfo(nodeJson.getPromptContent().getInfo(), user, + deptNameMap.getOrDefault(user.getDeptId(), StringUtils.EMPTY)); + nodeJson.getPromptContent().getInfo().add(buildInfoItem("审批状态", "待审批")); + } } - - // 按审批人分组去重,保留最新处理记录,最终转换成 List - List latestPerApprover = taskList.stream() - .collect(Collectors.collectingAndThen( - Collectors.toMap( - FlowHisTask::getApprover, - Function.identity(), - (oldTask, newTask) -> newTask.getUpdateTime().after(oldTask.getUpdateTime()) ? newTask : oldTask, - LinkedHashMap::new - ), - map -> new ArrayList<>(map.values()) - )); - - // 处理当前节点的扩展信息 - this.processNodeExtInfo(nodeJson, latestPerApprover, userMap, deptNameMap, dictType); } } @@ -183,10 +225,16 @@ public class FlwChartExtServiceImpl implements ChartExtService { continue; } - String deptName = deptNameMap.getOrDefault(userDTO.getDeptId(), StringUtils.EMPTY); + addUserInfo(info, userDTO, deptNameMap.getOrDefault(userDTO.getDeptId(), StringUtils.EMPTY)); - // 添加标题项,如:👤 张三(市场部) - info.add(new PromptContent.InfoItem() + info.add(buildInfoItem("审批状态", dictType.get(task.getFlowStatus()))); + info.add(buildInfoItem("审批耗时", DateUtils.formatBetweenBySecond(task.getUpdateTime(), task.getCreateTime()))); + info.add(buildInfoItem("办理时间", DateUtils.formatDateTime(task.getUpdateTime()))); + } + } + + private void addUserInfo(List info, UserDTO userDTO, String deptName) { + info.add(new PromptContent.InfoItem() .setPrefix(StringUtils.format("👥 {}({})", userDTO.getNickName(), deptName)) .setPrefixStyle(Map.of( "fontWeight", "bold", @@ -199,12 +247,7 @@ public class FlwChartExtServiceImpl implements ChartExtService { )) ); - // 添加具体信息项:账号、耗时、时间 - info.add(buildInfoItem("用户账号", userDTO.getUserName())); - info.add(buildInfoItem("审批状态", dictType.get(task.getFlowStatus()))); - info.add(buildInfoItem("审批耗时", DateUtils.formatBetweenBySecond(task.getUpdateTime(), task.getCreateTime()))); - info.add(buildInfoItem("办理时间", DateUtils.formatDateTime(task.getUpdateTime()))); - } + info.add(buildInfoItem("用户账号", userDTO.getUserName())); } /** diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwDefinitionServiceImpl.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwDefinitionServiceImpl.java index 3f9d167cd..b24d4fe37 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwDefinitionServiceImpl.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwDefinitionServiceImpl.java @@ -126,7 +126,7 @@ public class FlwDefinitionServiceImpl implements IFlwDefinitionService { if (CollUtil.isNotEmpty(flowNodes)) { String applyNodeCode = flwCommonService.applyNodeCode(id); for (FlowNode flowNode : flowNodes) { - if (StringUtils.isBlank(flowNode.getPermissionFlag()) && !applyNodeCode.equals(flowNode.getNodeCode()) && NodeType.BETWEEN.getKey().equals(flowNode.getNodeType())) { + if (requiresAssignee(flowNode, applyNodeCode) && StringUtils.isBlank(flowNode.getPermissionFlag())) { errorMsg.add(flowNode.getNodeName()); } } @@ -137,6 +137,12 @@ public class FlwDefinitionServiceImpl implements IFlwDefinitionService { return defService.publish(id); } + static boolean requiresAssignee(FlowNode node, String applyNodeCode) { + return NodeType.BETWEEN.getKey().equals(node.getNodeType()) + && !applyNodeCode.equals(node.getNodeCode()) + && StringUtils.isBlank(node.getEventType()); + } + /** * 导入流程定义 * diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwEventWaitServiceImpl.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwEventWaitServiceImpl.java new file mode 100644 index 000000000..b5ac228ee --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwEventWaitServiceImpl.java @@ -0,0 +1,242 @@ +package org.dromara.workflow.service.impl; + +import com.baomidou.lock.annotation.Lock4j; +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.domain.PageResult; +import org.dromara.common.core.exception.ServiceException; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.satoken.utils.LoginHelper; +import org.dromara.warm.flow.core.FlowEngine; +import org.dromara.warm.flow.core.constant.EventCons; +import org.dromara.warm.flow.core.dto.EventWaitQuery; +import org.dromara.warm.flow.core.entity.EventWait; +import org.dromara.warm.flow.core.entity.Instance; +import org.dromara.warm.flow.core.enums.EventType; +import org.dromara.warm.flow.core.exception.FlowException; +import org.dromara.warm.flow.core.utils.page.Page; +import org.dromara.warm.flow.core.workflow.command.ReviveEventCommand; +import org.dromara.warm.flow.core.workflow.context.WorkflowContext; +import org.dromara.workflow.common.ConditionalOnEnable; +import org.dromara.workflow.domain.bo.FlowEventWaitBo; +import org.dromara.workflow.domain.bo.FlowTimerTriggerBo; +import org.dromara.workflow.domain.vo.FlowEventWaitVo; +import org.dromara.workflow.service.IFlwEventWaitService; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 定时任务(事件等待记录)服务实现。 + *

条件解析与合法性校验由引擎 {@link EventWaitQuery} 与门面层完成,本类只负责参数透传、 + * 引擎实体到视图对象的装配(流程名 / 业务主键)与状态文案。手动补发复用引擎 + * {@code InsService#startByTimer},不读写等待记录,定时任务下次触发时间不受影响。

+ * + * @author may + * @since 2026/10/21 + */ +@ConditionalOnEnable +@RequiredArgsConstructor +@Slf4j +@Service +public class FlwEventWaitServiceImpl implements IFlwEventWaitService { + + /** + * 事件类型持久化值 → 页面文案。 + */ + private static final Map EVENT_TYPE_NAMES = new LinkedHashMap<>(); + + /** + * 等待状态持久化值 → 页面文案。 + */ + private static final Map STATUS_NAMES = new LinkedHashMap<>(); + + static { + EVENT_TYPE_NAMES.put(EventType.MESSAGE_START.getValue(), "消息启动"); + EVENT_TYPE_NAMES.put(EventType.TIMER_START.getValue(), "定时启动"); + EVENT_TYPE_NAMES.put(EventType.TIMER_CATCH.getValue(), "定时捕获"); + EVENT_TYPE_NAMES.put(EventType.MESSAGE_CATCH.getValue(), "消息捕获"); + EVENT_TYPE_NAMES.put(EventType.MESSAGE_THROW.getValue(), "消息抛出"); + EVENT_TYPE_NAMES.put(EventType.TIMER_BOUNDARY.getValue(), "定时边界"); + EVENT_TYPE_NAMES.put(EventType.MESSAGE_BOUNDARY.getValue(), "消息边界"); + EVENT_TYPE_NAMES.put(EventType.TERMINATE_END.getValue(), "终止结束"); + EVENT_TYPE_NAMES.put(EventType.MESSAGE_END.getValue(), "消息结束"); + EVENT_TYPE_NAMES.put(EventType.ERROR_CATCH.getValue(), "错误捕获"); + EVENT_TYPE_NAMES.put(EventType.ERROR_BOUNDARY.getValue(), "错误边界"); + + STATUS_NAMES.put(EventCons.STATUS_WAITING, "待触发"); + STATUS_NAMES.put(EventCons.STATUS_RETRY, "重试中"); + STATUS_NAMES.put(EventCons.STATUS_RUNNING, "执行中"); + STATUS_NAMES.put(EventCons.STATUS_CANCELLED, "已取消"); + STATUS_NAMES.put(EventCons.STATUS_DEAD, "已死"); + STATUS_NAMES.put(EventCons.STATUS_SUCCESS, "成功"); + } + + /** + * 分页查询事件等待记录。 + * + * @param bo 查询条件 + * @return 分页等待记录 + */ + @Override + public PageResult queryPageList(FlowEventWaitBo bo) { + Page page = FlowEngine.workflow().pageEventWaits(bo.toQuery()); + List items = page.getList() == null ? List.of() : page.getList(); + boolean timerEnabled = FlowEngine.getFlowConfig().isTimerEnabled(); + List rows = new ArrayList<>(items.size()); + for (EventWait wait : items) { + FlowEventWaitVo vo = toVo(wait); + vo.setTimerEnabled(timerEnabled); + rows.add(vo); + } + return PageResult.build(rows, page.getTotal()); + } + + /** + * 按同一组筛选条件统计各状态等待记录数量。 + * + * @param bo 查询条件 + * @return 各状态计数 + */ + @Override + public Map summary(FlowEventWaitBo bo) { + Map summary = new LinkedHashMap<>(); + summary.put("waiting", countByStatus(bo, EventCons.STATUS_WAITING)); + summary.put("retry", countByStatus(bo, EventCons.STATUS_RETRY)); + summary.put("running", countByStatus(bo, EventCons.STATUS_RUNNING)); + summary.put("dead", countByStatus(bo, EventCons.STATUS_DEAD)); + summary.put("cancelled", countByStatus(bo, EventCons.STATUS_CANCELLED)); + return summary; + } + + /** + * 查询单条等待记录详情。 + * + * @param id 事件等待记录主键 + * @return 等待记录详情,不存在时返回 null + */ + @Override + public FlowEventWaitVo queryById(Long id) { + EventWait wait = FlowEngine.eventWaitService().getById(id); + if (wait == null) { + return null; + } + FlowEventWaitVo vo = toVo(wait); + vo.setTimerEnabled(FlowEngine.getFlowConfig().isTimerEnabled()); + return vo; + } + + /** + * 手动补发一次定时启动任务(运维应急)。 + *

+ * 只做「按定义 + 事件节点直接发起」,不读写等待记录:定时任务的下次触发时间保持原样, + * 提前补发不会吞掉当天的正常触发。业务主键优先取调用方传入值,留空时用雪花 long 型 id + * (与宿主其它业务表主键同型,便于下游按 long 解析),天然不会与定时触发的 + * {@code T{waitId}_{计划时间}} 撞号。加分布式锁防止同机/多机双击连发。 + * + * @param bo 补发参数 + * @return 新建实例主键 + */ + @Override + @Lock4j(keys = {"#bo.waitId"}) + public Long manualTrigger(FlowTimerTriggerBo bo) { + if (bo == null || bo.getWaitId() == null) { + throw new ServiceException("事件等待记录主键不能为空"); + } + EventWait wait = FlowEngine.eventWaitService().getById(bo.getWaitId()); + if (wait == null) { + throw new ServiceException("事件等待记录不存在: " + bo.getWaitId()); + } + if (!EventType.TIMER_START.getValue().equals(wait.getEventType())) { + throw new ServiceException("只有定时启动任务支持手动触发,当前事件类型为 " + wait.getEventType()); + } + String finalBusinessId = StringUtils.isNotBlank(bo.getBusinessId()) + ? bo.getBusinessId().trim() + : String.valueOf(IdWorker.getId()); + WorkflowContext context = WorkflowContext.create(LoginHelper.getUserIdStr()); + try { + Instance instance = FlowEngine.insService().startByTimer(finalBusinessId, wait.getDefinitionId() + , wait.getNodeCode(), bo.getVariables(), context); + return instance.getId(); + } catch (FlowException e) { + log.warn("定时启动任务手动补发失败 waitId={} 原因={}", bo.getWaitId(), e.getMessage()); + throw new ServiceException(e.getMessage()); + } + } + + /** + * 复活已死的事件等待记录,复用引擎的状态校验、排期重算与乐观锁更新。 + * + * @param id 事件等待记录主键 + */ + @Override + public void revive(Long id) { + ReviveEventCommand command = new ReviveEventCommand(); + command.setEventWaitId(id); + try { + FlowEngine.workflow().reviveEvent(command); + } catch (FlowException e) { + log.warn("事件等待记录复活失败 waitId={} 原因={}", id, e.getMessage()); + throw new ServiceException(e.getMessage()); + } + } + + /** + * 统计某一状态下的等待记录数量。 + * + * @param bo 查询参数(其中状态条件被覆盖) + * @param status 等待状态 + * @return 记录数量 + */ + private long countByStatus(FlowEventWaitBo bo, String status) { + EventWaitQuery query = bo.toQuery(); + query.setStatuses(List.of(status)); + return FlowEngine.workflow().countEventWaits(query); + } + + /** + * 引擎等待记录 → 视图对象。 + * + * @param wait 引擎等待记录 + * @return 视图对象 + */ + private FlowEventWaitVo toVo(EventWait wait) { + FlowEventWaitVo vo = new FlowEventWaitVo(); + vo.setId(wait.getId()); + vo.setDefinitionId(wait.getDefinitionId()); + vo.setInstanceId(wait.getInstanceId()); + vo.setNodeCode(wait.getNodeCode()); + vo.setEventType(wait.getEventType()); + vo.setEventName(wait.getEventName()); + vo.setAttachedTaskId(wait.getAttachedTaskId()); + vo.setTriggerType(wait.getTriggerType()); + vo.setTriggerInterval(wait.getTriggerInterval()); + vo.setNextTriggerAt(wait.getNextTriggerAt()); + vo.setTimezone(wait.getTimezone()); + vo.setRepeatCount(wait.getRepeatCount()); + vo.setMaxRepeatCount(wait.getMaxRepeatCount()); + vo.setStatus(wait.getStatus()); + vo.setRetryCount(wait.getRetryCount()); + vo.setMaxRetryCount(wait.getMaxRetryCount()); + vo.setRetryAt(wait.getRetryAt()); + vo.setPayload(wait.getPayload()); + vo.setLastError(wait.getLastError()); + vo.setCreateTime(wait.getCreateTime()); + vo.setUpdateTime(wait.getUpdateTime()); + vo.setEventTypeName(EVENT_TYPE_NAMES.getOrDefault(wait.getEventType(), wait.getEventType())); + vo.setStatusName(STATUS_NAMES.getOrDefault(wait.getStatus(), wait.getStatus())); + if (wait.getDefinitionId() != null) { + var definition = FlowEngine.defService().getById(wait.getDefinitionId()); + vo.setFlowName(definition == null ? null : definition.getFlowName()); + } + if (wait.getInstanceId() != null) { + var instance = FlowEngine.insService().getById(wait.getInstanceId()); + vo.setBusinessId(instance == null ? null : instance.getBusinessId()); + } + return vo; + } +} diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwInstanceServiceImpl.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwInstanceServiceImpl.java index 8a6972e70..3b5cdb175 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwInstanceServiceImpl.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwInstanceServiceImpl.java @@ -20,8 +20,12 @@ import org.dromara.common.mybatis.core.query.LambdaJoinQueryBuilder; import org.dromara.common.mybatis.core.query.QueryBuilder; import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.warm.flow.core.FlowEngine; +import org.dromara.warm.flow.core.enums.TaskStatus; +import org.dromara.warm.flow.core.workflow.command.RevokeCommand; +import org.dromara.warm.flow.core.workflow.command.TerminateCommand; +import org.dromara.warm.flow.core.workflow.context.OperatorContext; import org.dromara.warm.flow.core.constant.ExceptionCons; -import org.dromara.warm.flow.core.dto.FlowParams; +import org.dromara.warm.flow.core.workflow.context.WorkflowContext; import org.dromara.warm.flow.core.entity.Definition; import org.dromara.warm.flow.core.entity.Instance; import org.dromara.warm.flow.core.entity.Task; @@ -36,7 +40,6 @@ import org.dromara.warm.flow.orm.entity.FlowTask; import org.dromara.warm.flow.orm.mapper.FlowHisTaskMapper; import org.dromara.warm.flow.orm.mapper.FlowInstanceMapper; import org.dromara.workflow.common.ConditionalOnEnable; -import org.dromara.workflow.common.enums.TaskStatusEnum; import org.dromara.workflow.domain.FlowInstanceBizExt; import org.dromara.workflow.domain.bo.FlowCancelBo; import org.dromara.workflow.domain.bo.FlowInstanceBo; @@ -267,13 +270,13 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService { throw new ServiceException("权限不足,无法撤销流程!"); } BusinessStatusEnum.checkCancelStatus(instance.getFlowStatus()); - FlowParams flowParams = FlowParams.build() - .message(bo.message()) - .flowStatus(BusinessStatusEnum.CANCEL.getStatus()) - .hisStatus(BusinessStatusEnum.CANCEL.getStatus()) - .handler(userIdStr) - .ignore(true); - taskService.revoke(instance.getId(), flowParams); + RevokeCommand command = new RevokeCommand(); + command.setOperator(new OperatorContext(userIdStr)); + command.getOperator().setIgnore(true); + command.setInstanceId(instance.getId()); + command.setMessage(bo.message()); + command.setTaskStatus(BusinessStatusEnum.CANCEL.getStatus()); + FlowEngine.workflow().revoke(command); return true; } @@ -300,15 +303,33 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService { */ @Override public Map flowHisTaskList(String businessId) { - FlowInstance flowInstance = this.selectInstByBusinessId(businessId); - if (ObjectUtil.isNull(flowInstance)) { + return flowHisTaskList(null, businessId); + } + + /** + * 按实例 ID、业务 ID 查询流程图及历史办理记录;同时提供时须指向同一实例。 + * + * @param instanceId 流程实例 ID,可为空 + * @param businessId 业务 ID,可为空 + * @return 流程图与办理记录 + */ + @Override + public Map flowHisTaskList(Long instanceId, String businessId) { + if (instanceId == null && StringUtils.isBlank(businessId)) { + throw new ServiceException("流程实例 ID 和业务 ID 不能同时为空"); + } + FlowInstance flowInstance = instanceId != null + ? this.selectInstById(instanceId) + : this.selectInstByBusinessId(businessId); + if (ObjectUtil.isNull(flowInstance) + || (StringUtils.isNotBlank(businessId) && !businessId.equals(flowInstance.getBusinessId()))) { throw new ServiceException(ExceptionCons.NOT_FOUNT_INSTANCE); } - Long instanceId = flowInstance.getId(); + Long resolvedInstanceId = flowInstance.getId(); // 先组装待审批任务(运行中的任务) List runningTaskVos = new ArrayList<>(); - List runningTasks = flwTaskService.selectByInstId(instanceId); + List runningTasks = flwTaskService.selectByInstId(resolvedInstanceId); if (CollUtil.isNotEmpty(runningTasks)) { runningTaskVos = BeanUtil.copyToList(runningTasks, FlowHisTaskVo.class); @@ -317,7 +338,8 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService { Map> taskUserMap = StreamUtils.groupByKey(associatedUsers, User::getAssociated); for (FlowHisTaskVo vo : runningTaskVos) { - vo.setFlowStatus(TaskStatusEnum.WAITING.getStatus()); + // 状态列保留待办任务自身的 flow_status(pending 待审批 / back 退回), + // 不要覆盖成实例维度编码:审批记录用任务字典渲染这一列。 vo.setUpdateTime(null); vo.setRunDuration(null); @@ -332,7 +354,7 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService { List hisTaskVos = new ArrayList<>(); List hisTasks = flowHisTaskMapper.selectList( QueryBuilder.lambda(FlowHisTask.class) - .eq(FlowHisTask::getInstanceId, instanceId) + .eq(FlowHisTask::getInstanceId, resolvedInstanceId) .eq(FlowHisTask::getNodeType, NodeType.BETWEEN.getKey()) .orderByDesc(FlowHisTask::getUpdateTime) .build() @@ -346,7 +368,7 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService { combinedList.addAll(runningTaskVos); combinedList.addAll(hisTaskVos); - return Map.of("list", combinedList, "instanceId", instanceId); + return Map.of("list", combinedList, "instanceId", resolvedInstanceId); } /** @@ -452,12 +474,14 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService { if (instance != null) { BusinessStatusEnum.checkInvalidStatus(instance.getFlowStatus()); } - FlowParams flowParams = FlowParams.build() - .message(bo.comment()) - .flowStatus(BusinessStatusEnum.INVALID.getStatus()) - .hisStatus(TaskStatusEnum.INVALID.getStatus()) - .ignore(true); - taskService.terminationByInsId(bo.id(), flowParams); + TerminateCommand command = new TerminateCommand(); + command.setOperator(new OperatorContext(LoginHelper.getUserIdStr())); + command.getOperator().setIgnore(true); + command.setInstanceId(bo.id()); + command.setMessage(bo.comment()); + command.setFlowStatus(BusinessStatusEnum.INVALID.getStatus()); + command.setTaskStatus(TaskStatus.INVALID.getKey()); + FlowEngine.workflow().terminate(command); return true; } } diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwNodeExtServiceImpl.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwNodeExtServiceImpl.java index 6a3b312bf..a1455faf6 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwNodeExtServiceImpl.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwNodeExtServiceImpl.java @@ -11,12 +11,10 @@ import org.dromara.common.core.utils.StringUtils; import org.dromara.common.json.utils.JsonUtils; import org.dromara.warm.flow.core.FlowEngine; import org.dromara.warm.flow.core.utils.CollUtil; -import org.dromara.warm.flow.core.utils.ExpressionUtil; import org.dromara.warm.flow.ui.service.NodeExtService; import org.dromara.warm.flow.ui.vo.NodeExt; import org.dromara.workflow.common.ConditionalOnEnable; import org.dromara.workflow.common.enums.ButtonPermissionEnum; -import org.dromara.workflow.common.enums.CopySettingEnum; import org.dromara.workflow.common.enums.NodeExtEnum; import org.dromara.workflow.common.enums.VariablesEnum; import org.dromara.workflow.domain.vo.ButtonPermissionVo; @@ -45,14 +43,6 @@ public class FlwNodeExtServiceImpl implements NodeExtService, IFlwNodeExtService static { CHILD_NODE_MAP = Map.of( - CopySettingEnum.class.getSimpleName(), - Map.of( - "label", "抄送对象", - "type", 5, - "must", false, - "multiple", false, - "desc", "设置该节点的抄送办理人" - ), VariablesEnum.class.getSimpleName(), Map.of( "label", "自定义参数", @@ -84,7 +74,7 @@ public class FlwNodeExtServiceImpl implements NodeExtService, IFlwNodeExtService List nodeExtList = new ArrayList<>(); // 构建基础设置页面 nodeExtList.add(buildNodeExt("wf_basic_tab", "基础设置", 1, - List.of(CopySettingEnum.class, VariablesEnum.class))); + List.of(VariablesEnum.class))); // 构建按钮权限页面 nodeExtList.add(buildNodeExt("wf_button_tab", "权限", 2, List.of(ButtonPermissionEnum.class))); @@ -192,21 +182,19 @@ public class FlwNodeExtServiceImpl implements NodeExtService, IFlwNodeExtService /** * 解析扩展属性 JSON 并构建 Node 扩展属性对象 *

- * 根据传入的 JSON 字符串,将扩展属性分为三类: + * 根据传入的 JSON 字符串,将扩展属性分为两类: * 1. ButtonPermissionEnum:解析为按钮权限列表,标记每个按钮是否勾选 - * 2. CopySettingEnum:解析为抄送对象 ID 集合 - * 3. VariablesEnum:解析为自定义参数 Map + * 2. VariablesEnum:解析为自定义参数 Map * *

示例 JSON: * [ * {"code": "ButtonPermissionEnum", "value": "back,termination"}, - * {"code": "CopySettingEnum", "value": "1,3,4,#{@spelRuleComponent.selectDeptLeaderById(#deptId", "#roleId)}"}, * {"code": "VariablesEnum", "value": "key1=value1,key2=value2"} * ] * * @param ext 扩展属性 JSON 字符串 * @param variable 流程变量 - * @return NodeExtVo 对象,封装按钮权限列表、抄送对象集合和自定义参数 Map + * @return NodeExtVo 对象,封装按钮权限列表和自定义参数 Map */ @Override public NodeExtVo parseNodeExt(String ext, Map variable) { @@ -240,22 +228,6 @@ public class FlwNodeExtServiceImpl implements NodeExtService, IFlwNodeExtService nodeExtVo.setButtonPermissions(buttonList); - } else if (CopySettingEnum.class.getSimpleName().equals(code)) { - List permissions = spelSmartSplit(value).stream() - .map(s -> { - List result = ExpressionUtil.evalVariable(s, variable); - if (CollUtil.isNotEmpty(result)) { - return result; - } - return Collections.singletonList(s); - }).filter(Objects::nonNull) - .flatMap(List::stream) - .distinct() - .collect(Collectors.toList()); - List copySettings = FlowEngine.permissionHandler().convertPermissions(permissions); - // 解析抄送对象 ID 集合 - nodeExtVo.setCopySettings(new HashSet<>(copySettings)); - } else if (VariablesEnum.class.getSimpleName().equals(code)) { // 解析自定义参数 // 将 key=value 字符串拆分为 Map @@ -272,93 +244,4 @@ public class FlwNodeExtServiceImpl implements NodeExtService, IFlwNodeExtService return nodeExtVo; } - /** - * 按逗号分割字符串,但保留 #{...} 表达式和字符串常量中的逗号 - * - * @param str 原始配置字符串 - * @return 智能分割后的片段列表 - */ - private static List spelSmartSplit(String str) { - List result = new ArrayList<>(); - if (str == null || str.trim().isEmpty()) { - return result; - } - - StringBuilder token = new StringBuilder(); - // #{...} 的嵌套深度 - int depth = 0; - // 是否在字符串常量中(" 或 ') - boolean inString = false; - // 当前字符串引号类型 - char stringQuote = 0; - - for (int i = 0; i < str.length(); i++) { - char c = str.charAt(i); - - // 检测进入 SpEL 表达式 #{...} - if (!inString && c == '#' && depth == 0 && checkNext(str, i, '{')) { - depth++; - token.append("#{"); - // 跳过 { - i++; - continue; - } - - // 在表达式中遇到 { 或 } 改变嵌套深度 - if (!inString && depth > 0) { - if (c == '{') { - depth++; - } else if (c == '}') { - depth--; - } - token.append(c); - continue; - } - - // 检测字符串开始/结束 - if (depth > 0 && (c == '"' || c == '\'')) { - if (!inString) { - inString = true; - stringQuote = c; - } else if (stringQuote == c) { - inString = false; - } - token.append(c); - continue; - } - - // 外层逗号才分割 - if (c == ',' && depth == 0 && !inString) { - String part = token.toString().trim(); - if (!part.isEmpty()) { - result.add(part); - } - token.setLength(0); - continue; - } - - token.append(c); - } - - // 添加最后一个 - String part = token.toString().trim(); - if (!part.isEmpty()) { - result.add(part); - } - - return result; - } - - /** - * 判断指定位置的下一个字符是否匹配预期字符。 - * - * @param str 原始字符串 - * @param index 当前索引 - * @param expected 期望匹配的字符 - * @return 匹配成功返回 {@code true} - */ - private static boolean checkNext(String str, int index, char expected) { - return index + 1 < str.length() && str.charAt(index + 1) == expected; - } - } diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwTaskAssigneeServiceImpl.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwTaskAssigneeServiceImpl.java index 8b6d9612d..8fc5aadc9 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwTaskAssigneeServiceImpl.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwTaskAssigneeServiceImpl.java @@ -10,11 +10,12 @@ import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.utils.DateUtils; import org.dromara.common.core.utils.StreamUtils; import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.system.api.*; import org.dromara.system.api.domain.DeptDTO; import org.dromara.system.api.domain.TaskAssigneeDTO; -import org.dromara.system.api.domain.UserDTO; import org.dromara.system.api.model.TaskAssigneeBody; +import org.dromara.warm.flow.core.spi.PermissionHandler; import org.dromara.warm.flow.ui.dto.HandlerFunDto; import org.dromara.warm.flow.ui.dto.HandlerQuery; import org.dromara.warm.flow.ui.dto.TreeFunDto; @@ -24,7 +25,6 @@ import org.dromara.warm.flow.ui.vo.HandlerSelectVo; import org.dromara.workflow.common.ConditionalOnEnable; import org.dromara.workflow.common.enums.TaskAssigneeEnum; import org.dromara.workflow.service.IFlwSpelService; -import org.dromara.workflow.service.IFlwTaskAssigneeService; import org.springframework.stereotype.Service; import java.util.*; @@ -39,7 +39,7 @@ import java.util.stream.Collectors; @Slf4j @RequiredArgsConstructor @Service -public class FlwTaskAssigneeServiceImpl implements IFlwTaskAssigneeService, HandlerSelectService { +public class FlwTaskAssigneeServiceImpl implements PermissionHandler, HandlerSelectService { private static final String DEFAULT_GROUP_NAME = "默认分组"; @@ -50,6 +50,69 @@ public class FlwTaskAssigneeServiceImpl implements IFlwTaskAssigneeService, Hand private final PostService postService; private final IFlwSpelService spelService; + /** + * 办理人权限标识,比如用户,角色,部门等,用于校验是否有权限办理任务 + * 后续在流程上下文中获取。 + * 返回当前用户权限集合 + * + * @return 当前用户权限集合 + */ + @Override + public List permissions() { + return Collections.singletonList(LoginHelper.getUserIdStr()); + } + + /** + * 获取当前办理人 + * + * @return 当前办理人 + */ + @Override + public String getHandler() { + return LoginHelper.getUserIdStr(); + } + + /** + * 将预设办理人转换为实际用户 ID 列表。 + * + * @param permissions 预设权限标识列表 + * @return 用户 ID 列表 + */ + @Override + public List convertPermissions(List permissions) { + if (CollUtil.isEmpty(permissions)) { + return permissions; + } + + Map> typeIdMap = new EnumMap<>(TaskAssigneeEnum.class); + for (String storageId : permissions) { + Pair parsed = this.parseStorageId(storageId); + if (parsed != null) { + typeIdMap.computeIfAbsent(parsed.getKey(), k -> new ArrayList<>()).add(parsed.getValue()); + } + } + + return typeIdMap.entrySet() + .stream() + .map(entry -> { + TaskAssigneeEnum type = entry.getKey(); + List longIds = StreamUtils.toList(entry.getValue(), Convert::toLong); + return switch (type) { + case USER -> longIds; + case ROLE -> userService.selectUserIdsByRoleIds(longIds); + case DEPT -> userService.selectUserIdsByDeptIds(longIds); + case POST -> userService.selectUserIdsByPostIds(longIds); + default -> Collections.emptyList(); + }; + }) + .filter(CollUtil::isNotEmpty) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .distinct() + .map(Convert::toStr) + .toList(); + } + /** * 获取办理人权限设置列表tabs页签 * @@ -192,45 +255,6 @@ public class FlwTaskAssigneeServiceImpl implements IFlwTaskAssigneeService, Hand .setCreateTime(assignee -> DateUtils.formatLocalDateTime(assignee.getCreateTime())); } - /** - * 批量解析多个存储标识符(storageIds),按类型分类并合并查询用户列表 - * 输入格式支持多个以逗号分隔的标识(如 "user:123,role:456,789") - * 会自动去重返回结果,非法格式的标识将被忽略 - * - * @param storageIds 多个存储标识符字符串(逗号分隔) - * @return 合并后的用户列表,去重后返回,非法格式的标识将被跳过 - */ - @Override - public List fetchUsersByStorageIds(String storageIds) { - if (StringUtils.isEmpty(storageIds)) { - return List.of(); - } - Map> typeIdMap = new EnumMap<>(TaskAssigneeEnum.class); - for (String storageId : StringUtils.str2List(storageIds, StringUtils.SEPARATOR, true, true)) { - Pair parsed = this.parseStorageId(storageId); - if (parsed != null) { - typeIdMap.computeIfAbsent(parsed.getKey(), k -> new ArrayList<>()).add(parsed.getValue()); - } - } - return this.getUsersByTypes(typeIdMap).stream() - .distinct() - .toList(); - } - - /** - * 按任务分配类型批量查询用户。 - * - * @param typeIdMap 任务分配类型与 id 列表映射 - * @return 用户列表 - */ - private List getUsersByTypes(Map> typeIdMap) { - return typeIdMap.entrySet().stream() - .map(entry -> this.getUsersByType(entry.getKey(), entry.getValue())) - .filter(CollUtil::isNotEmpty) - .flatMap(Collection::stream) - .toList(); - } - /** * 按任务分配类型批量查询名称映射。 * @@ -243,30 +267,6 @@ public class FlwTaskAssigneeServiceImpl implements IFlwTaskAssigneeService, Hand return nameMap; } - /** - * 根据指定的任务分配类型(TaskAssigneeEnum)和 ID 列表,获取对应的用户信息列表 - * - * @param type 任务分配类型,表示用户、角色、部门或其他(TaskAssigneeEnum 枚举值) - * @param ids 与指定分配类型关联的 ID 列表(例如用户ID、角色ID、部门ID等) - * @return 返回包含用户信息的列表。如果类型为用户(USER),则通过用户ID列表查询; - * 如果类型为角色(ROLE),则通过角色ID列表查询; - * 如果类型为部门(DEPT),则通过部门ID列表查询; - * 如果类型为岗位(POST)或无法识别的类型,则返回空列表 - */ - private List getUsersByType(TaskAssigneeEnum type, List ids) { - if (type == TaskAssigneeEnum.SPEL) { - return new ArrayList<>(); - } - List longIds = StreamUtils.toList(ids, Convert::toLong); - return switch (type) { - case USER -> userService.selectListByIds(longIds); - case ROLE -> userService.selectUsersByRoleIds(longIds); - case DEPT -> userService.selectUsersByDeptIds(longIds); - case POST -> userService.selectUsersByPostIds(longIds); - default -> new ArrayList<>(); - }; - } - /** * 根据任务分配类型和对应 ID 列表,批量查询名称映射关系 * diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwTaskServiceImpl.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwTaskServiceImpl.java index ea378a981..57bab4363 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwTaskServiceImpl.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwTaskServiceImpl.java @@ -18,20 +18,26 @@ import org.dromara.common.core.utils.StringUtils; import org.dromara.common.liteflow.utils.LiteFlowUtils; import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.query.QueryBuilder; -import org.dromara.common.mybatis.utils.IdGeneratorUtil; import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.system.api.UserService; import org.dromara.system.api.domain.UserDTO; import org.dromara.warm.flow.core.FlowEngine; +import org.dromara.warm.flow.core.enums.FlowStatus; +import org.dromara.warm.flow.core.enums.TaskStatus; +import org.dromara.warm.flow.core.service.copy.CopyConfig; +import org.dromara.warm.flow.core.workflow.command.RejectCommand; +import org.dromara.warm.flow.core.workflow.command.TerminateCommand; +import org.dromara.warm.flow.core.workflow.context.OperatorContext; import org.dromara.warm.flow.core.constant.ExceptionCons; import org.dromara.warm.flow.core.dto.FlowCombine; -import org.dromara.warm.flow.core.dto.FlowParams; +import org.dromara.warm.flow.core.workflow.context.WorkflowContext; import org.dromara.warm.flow.core.entity.*; import org.dromara.warm.flow.core.enums.NodeType; import org.dromara.warm.flow.core.enums.SkipType; import org.dromara.warm.flow.core.enums.UserType; import org.dromara.warm.flow.core.exception.FlowException; import org.dromara.warm.flow.core.service.*; +import org.dromara.warm.flow.core.spi.PermissionHandler; import org.dromara.warm.flow.core.utils.ExpressionUtil; import org.dromara.warm.flow.core.utils.MapUtil; import org.dromara.warm.flow.orm.entity.FlowHisTask; @@ -44,7 +50,6 @@ import org.dromara.warm.flow.orm.mapper.FlowTaskMapper; import org.dromara.workflow.api.domain.StartProcessReturnDTO; import org.dromara.workflow.common.ConditionalOnEnable; import org.dromara.workflow.common.enums.TaskAssigneeType; -import org.dromara.workflow.common.enums.TaskStatusEnum; import org.dromara.workflow.domain.bo.*; import org.dromara.workflow.domain.context.CompleteTaskContext; import org.dromara.workflow.domain.context.StartProcessContext; @@ -58,7 +63,6 @@ import org.dromara.workflow.mapper.FlwHisTaskMapper; import org.dromara.workflow.mapper.FlwTaskMapper; import org.dromara.workflow.service.IFlwCommonService; import org.dromara.workflow.service.IFlwNodeExtService; -import org.dromara.workflow.service.IFlwTaskAssigneeService; import org.dromara.workflow.service.IFlwTaskService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -100,7 +104,6 @@ public class FlwTaskServiceImpl implements IFlwTaskService { private final FlwHisTaskMapper flwHisTaskMapper; private final FlwCategoryMapper flwCategoryMapper; private final FlowNodeMapper flowNodeMapper; - private final IFlwTaskAssigneeService flwTaskAssigneeService; private final IFlwCommonService flwCommonService; private final IFlwNodeExtService flwNodeExtService; @@ -145,44 +148,21 @@ public class FlwTaskServiceImpl implements IFlwTaskService { if (CollUtil.isEmpty(flowCopyList)) { return; } - // 添加抄送人记录 - List flowHisTasks = flowHisTaskMapper.selectList( - QueryBuilder.lambda(FlowHisTask.class) - .eq(FlowHisTask::getTaskId, task.getId()) - .build()); - if (CollUtil.isEmpty(flowHisTasks)) { - throw new ServiceException("流程历史任务不存在,无法添加抄送记录"); + WorkflowContext context = WorkflowContext.create(LoginHelper.getUserIdStr()); + context.setMessage("【抄送给】" + StreamUtils.join(flowCopyList, FlowCopyBo::getNickName)); + // 抄送留痕交给引擎:按「同一待办 + 同一人」幂等,抄送人挂在抄送历史主键上,办结后仍可补抄送。 + HisTask copyHis = taskService.copy(task.getId() + , StreamUtils.toList(flowCopyList, x -> Convert.toStr(x.getUserId())), context); + if (ObjectUtil.isNull(copyHis)) { + return; } - FlowHisTask flowHisTask = flowHisTasks.getFirst(); - FlowNode flowNode = new FlowNode(); - flowNode.setNodeCode(flowHisTask.getTargetNodeCode()); - flowNode.setNodeName(flowHisTask.getTargetNodeName()); - //生成新的任务id - long taskId = IdGeneratorUtil.nextLongId(); - task.setId(taskId); - task.setNodeName("【抄送】" + task.getNodeName()); - Date updateTime = new Date(flowHisTask.getUpdateTime().getTime() - 1000); - FlowParams flowParams = FlowParams.build() - .skipType(SkipType.NONE.getKey()) - .hisStatus(TaskStatusEnum.COPY.getStatus()) - .message("【抄送给】" + StreamUtils.join(flowCopyList, FlowCopyBo::getNickName)); - HisTask hisTask = hisTaskService.setSkipHisTask(task, flowNode, flowParams); - hisTask.setCreateTime(updateTime); - hisTask.setUpdateTime(updateTime); - hisTaskService.save(hisTask); - List userList = StreamUtils.toList(flowCopyList, x -> - new FlowUser() - .setType(TaskAssigneeType.COPY.getCode()) - .setProcessedBy(Convert.toStr(x.getUserId())) - .setAssociated(taskId)); - // 批量保存抄送人员 - FlowEngine.userService().saveBatch(userList); - // 抄送消息进入“我的抄送”,不和待办列表混用。 + // 抄送消息进入“我的抄送”,不和待办列表混用;只提醒本次真正新增的人。 flwCommonService.sendMessage( List.of(org.dromara.workflow.common.enums.MessageTypeEnum.SYSTEM_MESSAGE.getCode()), "您收到一条新的流程抄送,请及时查看。", "单据抄送提醒", - userService.selectListByIds(StreamUtils.toList(flowCopyList, FlowCopyBo::getUserId)), + userService.selectListByIds(StreamUtils.toList( + StringUtils.str2List(copyHis.getCollaborator(), StringUtils.SEPARATOR, true, true), Convert::toLong)), PATH_TASK_COPY ); } @@ -315,15 +295,16 @@ public class FlwTaskServiceImpl implements IFlwTaskService { variable.put(MESSAGE_TYPE, messageType); variable.put(MESSAGE_NOTICE, notice); - FlowParams flowParams = FlowParams.build() - .nodeCode(bo.getNodeCode()) - .variable(variable) - .message(message) - .skipType(SkipType.REJECT.getKey()) - .flowStatus(applyNodeCode.equals(bo.getNodeCode()) ? TaskStatusEnum.BACK.getStatus() : TaskStatusEnum.WAITING.getStatus()) - .hisStatus(TaskStatusEnum.BACK.getStatus()) - .hisTaskExt(bo.getFileId()); - taskService.skip(task.getId(), flowParams); + RejectCommand command = new RejectCommand(); + command.setOperator(new OperatorContext(LoginHelper.getUserIdStr())); + command.setTaskId(task.getId()); + command.setTargetNodeCode(bo.getNodeCode()); + command.setVariables(variable); + command.setMessage(message); + command.setExt(bo.getFileId()); + command.setFlowStatus(applyNodeCode.equals(bo.getNodeCode()) ? FlowStatus.BACK.getKey() : FlowStatus.WAITING.getKey()); + command.setTaskStatus(TaskStatus.BACK.getKey()); + FlowEngine.workflow().reject(command); return true; } @@ -479,11 +460,11 @@ public class FlwTaskServiceImpl implements IFlwTaskService { if (ObjectUtil.isNotNull(instance)) { BusinessStatusEnum.checkInvalidStatus(instance.getFlowStatus()); } - FlowParams flowParams = FlowParams.build() - .message(bo.comment()) - .flowStatus(BusinessStatusEnum.TERMINATION.getStatus()) - .hisStatus(TaskStatusEnum.TERMINATION.getStatus()); - taskService.termination(taskId, flowParams); + TerminateCommand command = new TerminateCommand(); + command.setOperator(new OperatorContext(LoginHelper.getUserIdStr())); + command.setTaskId(taskId); + command.setMessage(bo.comment()); + FlowEngine.workflow().terminate(command); return true; } @@ -538,12 +519,13 @@ public class FlwTaskServiceImpl implements IFlwTaskService { } else { flowTaskVo.setButtonList(new ArrayList<>()); } - if (CollUtil.isNotEmpty(nodeExtVo.getCopySettings())) { - List list = StreamUtils.toList(nodeExtVo.getCopySettings(), x -> new FlowCopyVo(Convert.toLong(x))); - flowTaskVo.setCopyList(list); - } else { - flowTaskVo.setCopyList(new ArrayList<>()); + // 抄送人读引擎节点配置 node_config.copy,展开规则与流转时一致(resolveCopyHandlers)。 + List copyHandlers = CopyConfig.parse(flowNode.getNodeConfig()).getHandlers(); + PermissionHandler permissionHandler = FlowEngine.permissionHandler(); + if (CollUtil.isNotEmpty(copyHandlers) && ObjectUtil.isNotNull(permissionHandler)) { + copyHandlers = permissionHandler.resolveCopyHandlers(copyHandlers); } + flowTaskVo.setCopyList(StreamUtils.toList(copyHandlers, x -> new FlowCopyVo(Convert.toLong(x)))); if (CollUtil.isNotEmpty(nodeExtVo.getVariables())) { flowTaskVo.setVarList(nodeExtVo.getVariables()); } else { @@ -582,24 +564,20 @@ public class FlwTaskServiceImpl implements IFlwTaskService { if (CollUtil.isEmpty(nextNodeList)) { return new ArrayList<>(); } - List nextFlowNodes = BeanUtil.copyToList(nextNodeList, FlowNode.class); // 只获取中间节点 - nextFlowNodes = StreamUtils.filter(nextFlowNodes, node -> NodeType.BETWEEN.getKey().equals(node.getNodeType())); - if (CollUtil.isNotEmpty(nextNodeList)) { - //构建以下节点数据 - List buildNextTaskList = StreamUtils.toList(nextNodeList, node -> taskService.addTask(node, instance, definition, FlowParams.build())); - //办理人变量替换 - ExpressionUtil.evalVariable(buildNextTaskList, FlowParams.build().variable(mergeVariable)); - for (FlowNode flowNode : nextFlowNodes) { - StreamUtils.findFirst(buildNextTaskList, t -> t.getNodeCode().equals(flowNode.getNodeCode())) - .ifPresent(first -> { - List users; - if (CollUtil.isNotEmpty(first.getPermissionList()) - && CollUtil.isNotEmpty(users = flwTaskAssigneeService.fetchUsersByStorageIds(StringUtils.joinComma(first.getPermissionList())))) { - flowNode.setPermissionFlag(StreamUtils.join(users, e -> Convert.toStr(e.getUserId()))); - } - }); - } + nextNodeList = StreamUtils.filter(nextNodeList, node -> NodeType.BETWEEN.getKey().equals(node.getNodeType())); + if (CollUtil.isEmpty(nextNodeList)) { + return new ArrayList<>(); + } + List nextFlowNodes = BeanUtil.copyToList(nextNodeList, FlowNode.class); + //构建以下节点数据 + List buildNextTaskList = StreamUtils.toList(nextNodeList, node -> taskService.addTask(node, instance + , definition, WorkflowContext.create(), SkipType.PASS.getKey())); + //办理人变量替换 + ExpressionUtil.evalVariable(buildNextTaskList, mergeVariable, null, false); + Map> map = StreamUtils.toMap(buildNextTaskList, Task::getNodeCode, Task::getPermissionList); + for (FlowNode node : nextFlowNodes) { + node.setPermissionFlag(StringUtils.joinComma(map.get(node.getNodeCode()))); } return nextFlowNodes; } @@ -756,6 +734,19 @@ public class FlwTaskServiceImpl implements IFlwTaskService { if (taskAssignee) { return; } + // 抄送人挂在抄送历史主键上(引擎 CopyAdvancer),不挂在待办 id 下, + // 所以要先按待办找到抄送历史,再按历史主键查抄送接收人。 + List copyHisList = FlowEngine.hisTaskService() + .list(FlowEngine.newHisTask().setTaskId(task.getId()).setFlowStatus(TaskStatus.COPY.getKey())); + List copyHisIds = StreamUtils.toList(copyHisList, HisTask::getId); + if (CollUtil.isNotEmpty(copyHisIds)) { + boolean copyReceiver = FlowEngine.userService().getByAssociateds(copyHisIds).stream() + .anyMatch(user -> Objects.equals(userId, user.getProcessedBy()) + && UserType.COPY.getKey().equals(Convert.toStr(user.getType()))); + if (copyReceiver) { + return; + } + } Instance instance = insService.getById(task.getInstanceId()); if (ObjectUtil.isNotNull(instance) && Objects.equals(instance.getCreateBy(), userId)) { return; diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/TestLeaveServiceImpl.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/TestLeaveServiceImpl.java index add317aaa..605dcc53f 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/TestLeaveServiceImpl.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/TestLeaveServiceImpl.java @@ -17,6 +17,9 @@ import org.dromara.common.core.utils.StreamUtils; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.mybatis.core.domain.BaseEntity; import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.warm.flow.core.FlowEngine; +import org.dromara.warm.flow.core.entity.Definition; +import org.dromara.warm.flow.core.entity.Instance; import org.dromara.workflow.api.WorkflowService; import org.dromara.workflow.api.domain.StartProcessDTO; import org.dromara.workflow.api.event.ProcessDeleteEvent; @@ -28,6 +31,7 @@ import org.dromara.workflow.domain.TestLeave; import org.dromara.workflow.domain.bo.TestLeaveBo; import org.dromara.workflow.domain.vo.TestLeaveVo; import org.dromara.workflow.mapper.TestLeaveMapper; +import org.dromara.workflow.service.IFlwInstanceService; import org.dromara.workflow.service.ITestLeaveService; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; @@ -52,6 +56,7 @@ public class TestLeaveServiceImpl implements ITestLeaveService { private final TestLeaveMapper leaveMapper; private final WorkflowService workflowService; + private final IFlwInstanceService flwInstanceService; /** * spel条件表达:判断小于2 @@ -71,7 +76,19 @@ public class TestLeaveServiceImpl implements ITestLeaveService { */ @Override public TestLeaveVo queryById(Long id) { - return leaveMapper.selectVoById(id); + TestLeaveVo vo = leaveMapper.selectVoById(id); + if (ObjectUtil.isNull(vo)) { + return null; + } + // 撤回 / 退回后修改再提交走的是已有实例的续提交,前端需要拿到原流程编码 + Instance instance = flwInstanceService.selectInstByBusinessId(Convert.toStr(id)); + if (ObjectUtil.isNotNull(instance)) { + Definition definition = FlowEngine.defService().getById(instance.getDefinitionId()); + if (ObjectUtil.isNotNull(definition)) { + vo.setFlowCode(definition.getFlowCode()); + } + } + return vo; } /** @@ -205,14 +222,22 @@ public class TestLeaveServiceImpl implements ITestLeaveService { /** * 总体流程监听(例如: 草稿,撤销,退回,作废,终止,已完成等) * 正常使用只需#processEvent.flowCode=='leave1' - * 示例为了方便则使用startsWith匹配了全部示例key + * 示例流程编码已切换为引擎自带的 demo_ 前缀,故这里按 demo 匹配 * * @param processEvent 参数 */ - @EventListener(condition = "#processEvent.flowCode.startsWith('leave')") + @EventListener(condition = "#processEvent.flowCode.startsWith('demo')") public void processHandler(ProcessEvent processEvent) { log.info("当前任务执行了{}", processEvent.toString()); - TestLeave testLeave = leaveMapper.selectById(Convert.toLong(processEvent.getBusinessId())); + Long leaveId = Convert.toLong(processEvent.getBusinessId()); + if (leaveId == null) { + return; + } + TestLeave testLeave = leaveMapper.selectById(leaveId); + if (ObjectUtil.isNull(testLeave)) { + // 其他演示流程没有对应的请假记录。 + return; + } testLeave.setStatus(processEvent.getStatus()); // 用于例如审批附件 审批意见等 存储到业务表内 自行根据业务实现存储流程 Map params = processEvent.getParams(); @@ -247,7 +272,7 @@ public class TestLeaveServiceImpl implements ITestLeaveService { * * @param processTaskEvent 参数 */ - @EventListener(condition = "#processTaskEvent.flowCode.startsWith('leave')") + @EventListener(condition = "#processTaskEvent.flowCode.startsWith('demo')") public void processTaskHandler(ProcessTaskEvent processTaskEvent) { log.info("当前任务创建了{}", processTaskEvent.toString()); } @@ -255,14 +280,18 @@ public class TestLeaveServiceImpl implements ITestLeaveService { /** * 监听删除流程事件 * 正常使用只需#processDeleteEvent.flowCode=='leave1' - * 示例为了方便则使用startsWith匹配了全部示例key + * 示例流程编码已切换为引擎自带的 demo_ 前缀,故这里按 demo 匹配 * * @param processDeleteEvent 参数 */ - @EventListener(condition = "#processDeleteEvent.flowCode.startsWith('leave')") + @EventListener(condition = "#processDeleteEvent.flowCode.startsWith('demo')") public void processDeleteHandler(ProcessDeleteEvent processDeleteEvent) { log.info("监听删除流程事件,当前任务执行了{}", processDeleteEvent.toString()); - TestLeave testLeave = leaveMapper.selectById(Convert.toLong(processDeleteEvent.getBusinessId())); + Long leaveId = Convert.toLong(processDeleteEvent.getBusinessId()); + if (leaveId == null) { + return; + } + TestLeave testLeave = leaveMapper.selectById(leaveId); if (ObjectUtil.isNull(testLeave)) { return; } diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/WorkflowServiceImpl.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/WorkflowServiceImpl.java index 3955d163a..b1f5e23e6 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/WorkflowServiceImpl.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/WorkflowServiceImpl.java @@ -2,13 +2,20 @@ package org.dromara.workflow.service.impl; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.toolkit.IdWorker; import lombok.RequiredArgsConstructor; +import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.utils.StringUtils; +import org.dromara.warm.flow.core.FlowEngine; +import org.dromara.warm.flow.core.workflow.context.WorkflowContext; import org.dromara.warm.flow.orm.entity.FlowInstance; import org.dromara.workflow.api.WorkflowService; import org.dromara.workflow.api.domain.CompleteTaskDTO; +import org.dromara.workflow.api.domain.StartMessageEventDTO; import org.dromara.workflow.api.domain.StartProcessDTO; import org.dromara.workflow.api.domain.StartProcessReturnDTO; +import org.dromara.workflow.api.domain.StartTimerEventDTO; +import org.dromara.workflow.api.domain.TriggerMessageEventDTO; import org.dromara.workflow.common.ConditionalOnEnable; import org.dromara.workflow.common.enums.MessageTypeEnum; import org.dromara.workflow.domain.FlowInstanceBizExt; @@ -170,4 +177,83 @@ public class WorkflowServiceImpl implements WorkflowService { taskBo.setHandler(startProcess.getHandler()); return flwTaskService.completeTask(taskBo); } + + /** + * 外部消息按流程编码和事件名称启动新实例。 + * + * @param request 消息启动参数,业务 ID 由调用方提供 + * @return 新建流程实例 ID + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Long startByMessageEvent(StartMessageEventDTO request) { + if (request == null) { + throw new ServiceException("消息启动参数不能为空"); + } + return FlowEngine.insService().startByMessage(request.getBusinessId(), request.getFlowCode(), + request.getEventName(), request.getPayload(), WorkflowContext.create(request.getHandler())).getId(); + } + + /** + * 触发实例中等待的消息捕获或消息边界事件。重复投递时引擎保持实例不变。 + * + * @param request 消息事件参数 + * @return 流程实例 ID + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Long triggerMessageEvent(TriggerMessageEventDTO request) { + if (request == null) { + throw new ServiceException("消息事件参数不能为空"); + } + return FlowEngine.insService().triggerMessage(request.getInstanceId(), request.getEventName(), + request.getPayload(), WorkflowContext.create(request.getHandler())).getId(); + } + + /** + * 手动发起定时启动事件,不改变等待记录及下一次自动触发时间。 + * + * @param request 定时启动参数,业务 ID 留空时生成数字雪花 ID + * @return 新建流程实例 ID + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Long startByTimerEvent(StartTimerEventDTO request) { + if (request == null) { + throw new ServiceException("定时启动参数不能为空"); + } + String businessId = StringUtils.isNotBlank(request.getBusinessId()) + ? request.getBusinessId().trim() : String.valueOf(IdWorker.getId()); + return FlowEngine.insService().startByTimer(businessId, request.getDefinitionId(), + request.getNodeCode(), request.getPayload(), WorkflowContext.create(request.getHandler())).getId(); + } + + /** + * 从实例级错误捕获节点处理外部错误。 + * + * @param instanceId 流程实例 ID + * @param errorCode 错误码,精确匹配优先于通配 + * @param errorMessage 错误描述 + * @return 错误事件已被处理时返回 true + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean throwInstanceErrorEvent(Long instanceId, String errorCode, String errorMessage) { + return FlowEngine.insService().throwError(instanceId, errorCode, errorMessage); + } + + /** + * 从当前待办的错误边界节点处理外部错误。 + * + * @param taskId 当前待办任务 ID + * @param errorCode 错误码,精确匹配优先于通配 + * @param errorMessage 错误描述 + * @return 错误事件已被处理时返回 true + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean throwTaskErrorEvent(Long taskId, String errorCode, String errorMessage) { + return FlowEngine.taskService().throwError(taskId, errorCode, errorMessage); + } + } diff --git a/ruoyi-modules/ruoyi-workflow/src/main/resources/liteflow/task-chain.el.xml b/ruoyi-modules/ruoyi-workflow/src/main/resources/liteflow/task-chain.el.xml index 772243388..77d82afd5 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/resources/liteflow/task-chain.el.xml +++ b/ruoyi-modules/ruoyi-workflow/src/main/resources/liteflow/task-chain.el.xml @@ -25,17 +25,12 @@ 办理任务链路: 1. 加载任务实例、补齐变量并解析办理人 2. 构建参数并执行当前任务跳转 - 3. 开启自动审批时继续办理当前处理人的后续任务 + 自动审批由引擎按流程定义配置 def_config.autoPass 在办理链内完成,宿主不再单独编排。 --> THEN( completePrepare, - completeExecute, - IF( - completeNeedAutoPass, - completeAutoPass, - noop - ) + completeExecute ); diff --git a/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/demo/DemoTimerInspectHandlerTest.java b/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/demo/DemoTimerInspectHandlerTest.java new file mode 100644 index 000000000..f2edb2a94 --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/demo/DemoTimerInspectHandlerTest.java @@ -0,0 +1,56 @@ +package org.dromara.workflow.demo; + +import org.dromara.warm.flow.core.FlowEngine; +import org.dromara.warm.flow.core.config.WarmFlow; +import org.dromara.warm.flow.core.entity.Definition; +import org.dromara.warm.flow.core.entity.EventWait; +import org.dromara.warm.flow.core.enums.EventType; +import org.dromara.warm.flow.core.service.DefService; +import org.dromara.warm.flow.core.service.InsService; +import org.dromara.warm.flow.core.workflow.context.WorkflowContext; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class DemoTimerInspectHandlerTest { + + @Test + void timerStartUsesNumericBusinessId() { + EventWait wait = mock(EventWait.class); + when(wait.getEventType()).thenReturn(EventType.TIMER_START.getValue()); + when(wait.getDefinitionId()).thenReturn(12L); + when(wait.getNodeCode()).thenReturn("timerStart"); + Definition definition = mock(Definition.class); + when(definition.getFlowCode()).thenReturn("demo_timer_start"); + DefService defService = mock(DefService.class); + when(defService.getById(12L)).thenReturn(definition); + InsService insService = mock(InsService.class); + WarmFlow flowConfig = mock(WarmFlow.class); + when(flowConfig.getSystemHandler()).thenReturn("0"); + + try (MockedStatic engine = mockStatic(FlowEngine.class)) { + engine.when(FlowEngine::defService).thenReturn(defService); + engine.when(FlowEngine::insService).thenReturn(insService); + engine.when(FlowEngine::getFlowConfig).thenReturn(flowConfig); + + assertDoesNotThrow(() -> new DemoTimerInspectHandler().handle(wait)); + + ArgumentCaptor businessId = ArgumentCaptor.forClass(String.class); + ArgumentCaptor context = ArgumentCaptor.forClass(WorkflowContext.class); + verify(insService).startByTimer(businessId.capture(), eq(12L), eq("timerStart"), isNull(), context.capture()); + assertTrue(businessId.getValue().matches("[0-9]+")); + assertTrue(Long.parseLong(businessId.getValue()) > 0); + assertEquals("0", context.getValue().getHandler()); + } + } +} diff --git a/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/FlwChartExtServiceImplTest.java b/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/FlwChartExtServiceImplTest.java new file mode 100644 index 000000000..edd16e697 --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/FlwChartExtServiceImplTest.java @@ -0,0 +1,81 @@ +package org.dromara.workflow.service.impl; + +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.apache.ibatis.session.Configuration; +import org.dromara.common.core.service.DictService; +import org.dromara.system.api.DeptService; +import org.dromara.system.api.UserService; +import org.dromara.system.api.domain.UserDTO; +import org.dromara.warm.flow.core.dto.DefJson; +import org.dromara.warm.flow.core.dto.NodeJson; +import org.dromara.warm.flow.orm.entity.FlowInstance; +import org.dromara.warm.flow.orm.entity.FlowTask; +import org.dromara.warm.flow.orm.entity.FlowUser; +import org.dromara.warm.flow.orm.mapper.FlowHisTaskMapper; +import org.dromara.warm.flow.orm.mapper.FlowTaskMapper; +import org.dromara.warm.flow.orm.mapper.FlowUserMapper; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class FlwChartExtServiceImplTest { + + @Test + void showsCurrentAssigneeWithoutApprovalHistory() { + TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new Configuration(), "test"), FlowUser.class); + UserService userService = mock(UserService.class); + DeptService deptService = mock(DeptService.class); + FlowHisTaskMapper historyMapper = mock(FlowHisTaskMapper.class); + FlowTaskMapper taskMapper = mock(FlowTaskMapper.class); + FlowUserMapper flowUserMapper = mock(FlowUserMapper.class); + DictService dictService = mock(DictService.class); + FlwChartExtServiceImpl service = new FlwChartExtServiceImpl( + userService, deptService, historyMapper, taskMapper, flowUserMapper, dictService); + ReflectionTestUtils.setField(service, "nodeTooltip", true); + + NodeJson node = new NodeJson().setNodeCode("approve").setNodeName("部门审批"); + DefJson definition = new DefJson() + .setFlowName("请假申请") + .setInstance(new FlowInstance().setId(1L)) + .setNodeList(List.of(node)); + service.initPromptContent(definition); + + when(historyMapper.selectList(any())).thenReturn(List.of()); + when(taskMapper.selectList(any())).thenReturn(List.of( + new FlowTask().setId(10L).setNodeCode("approve"))); + when(flowUserMapper.selectList(any())).thenReturn(List.of( + new FlowUser().setAssociated(10L).setType("1").setProcessedBy("7"))); + UserDTO user = new UserDTO(); + user.setUserId(7L); + user.setDeptId(3L); + user.setNickName("张三"); + user.setUserName("zhangsan"); + when(userService.selectListByIds(any())).thenReturn(List.of(user)); + when(deptService.selectDeptNamesByIds(any())).thenReturn(Map.of(3L, "研发部")); + when(dictService.getAllDictByDictType(any())).thenReturn(Map.of()); + + service.execute(definition); + + var info = node.getPromptContent().getInfo(); + assertTrue(info.stream().anyMatch(item -> item.getPrefix().contains("张三") + && item.getPrefix().contains("研发部"))); + assertTrue(info.stream().anyMatch(item -> "用户账号: ".equals(item.getPrefix()) + && "zhangsan".equals(item.getContent()))); + assertTrue(info.stream().anyMatch(item -> "审批状态: ".equals(item.getPrefix()) + && "待审批".equals(item.getContent()))); + verify(userService).selectListByIds(argThat(ids -> ids.size() == 1 && ids.contains(7L))); + verify(deptService).selectDeptNamesByIds(argThat(ids -> ids.size() == 1 && ids.contains(3L))); + assertEquals(4, info.size()); + } +} diff --git a/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/FlwDefinitionServiceImplTest.java b/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/FlwDefinitionServiceImplTest.java new file mode 100644 index 000000000..f2b73a448 --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/FlwDefinitionServiceImplTest.java @@ -0,0 +1,39 @@ +package org.dromara.workflow.service.impl; + +import org.dromara.warm.flow.core.enums.NodeType; +import org.dromara.warm.flow.orm.entity.FlowNode; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class FlwDefinitionServiceImplTest { + + @Test + void eventNodesDoNotRequireAssignee() { + for (String eventType : new String[]{"MESSAGE_START", "TIMER_CATCH", "ERROR_BOUNDARY", "TERMINATE_END"}) { + FlowNode node = new FlowNode() + .setNodeType(NodeType.BETWEEN.getKey()) + .setNodeCode("event") + .setEventType(eventType); + assertFalse(FlwDefinitionServiceImpl.requiresAssignee(node, "apply"), eventType); + } + } + + @Test + void manualApprovalStillRequiresAssignee() { + FlowNode approval = new FlowNode() + .setNodeType(NodeType.BETWEEN.getKey()) + .setNodeCode("approve"); + assertTrue(FlwDefinitionServiceImpl.requiresAssignee(approval, "apply")); + assertFalse(FlwDefinitionServiceImpl.requiresAssignee(approval, "approve")); + } + + @Test + void serviceTaskDoesNotRequireAssignee() { + FlowNode service = new FlowNode() + .setNodeType(NodeType.SERVICE.getKey()) + .setNodeCode("service"); + assertFalse(FlwDefinitionServiceImpl.requiresAssignee(service, "apply")); + } +} diff --git a/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/FlwEventWaitServiceImplTest.java b/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/FlwEventWaitServiceImplTest.java new file mode 100644 index 000000000..939e6595b --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/FlwEventWaitServiceImplTest.java @@ -0,0 +1,62 @@ +package org.dromara.workflow.service.impl; + +import org.dromara.common.core.exception.ServiceException; +import org.dromara.warm.flow.core.FlowEngine; +import org.dromara.warm.flow.core.constant.EventCons; +import org.dromara.warm.flow.core.entity.EventWait; +import org.dromara.warm.flow.core.enums.EventType; +import org.dromara.warm.flow.core.service.EventWaitService; +import org.dromara.warm.flow.core.service.impl.EventWaitServiceImpl; +import org.dromara.warm.flow.core.workflow.WorkflowServiceImpl; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.MockedStatic; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +class FlwEventWaitServiceImplTest { + + @ParameterizedTest + @ValueSource(strings = {"WAITING", "RETRY", "RUNNING", "CANCELLED", "SUCCESS"}) + void recoveryRejectsRecordsThatAreNotDead(String status) { + ServiceException error = recover(status, EventType.TIMER_CATCH); + assertTrue(error.getMessage().contains("DEAD"), error.getMessage()); + } + + @Test + void deadTimerStartMustBeRebuiltFromDefinition() { + ServiceException error = recover(EventCons.STATUS_DEAD, EventType.TIMER_START); + assertTrue(error.getMessage().contains("定时启动"), error.getMessage()); + } + + @Test + void deadEventCannotResumeWithoutItsInstance() { + ServiceException error = recover(EventCons.STATUS_DEAD, EventType.MESSAGE_CATCH); + assertTrue(error.getMessage().contains("实例"), error.getMessage()); + } + + /** + * 使用实际 2.1.1 门面和事件服务验证恢复约束,避免宿主绕过引擎直接改状态。 + */ + private ServiceException recover(String status, EventType type) { + EventWait wait = mock(EventWait.class); + when(wait.getStatus()).thenReturn(status); + when(wait.getEventType()).thenReturn(type.getValue()); + when(wait.getAttachedTaskId()).thenReturn(null); + when(wait.getInstanceId()).thenReturn(null); + EventWaitService eventService = spy(new EventWaitServiceImpl()); + doReturn(wait).when(eventService).getById(12L); + try (MockedStatic engine = mockStatic(FlowEngine.class)) { + engine.when(FlowEngine::workflow).thenReturn(new WorkflowServiceImpl()); + engine.when(FlowEngine::eventWaitService).thenReturn(eventService); + return assertThrows(ServiceException.class, () -> new FlwEventWaitServiceImpl().revive(12L)); + } + } +} diff --git a/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/FlwInstanceProgressTest.java b/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/FlwInstanceProgressTest.java new file mode 100644 index 000000000..f73b1e6d3 --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/FlwInstanceProgressTest.java @@ -0,0 +1,80 @@ +package org.dromara.workflow.service.impl; + +import org.dromara.common.core.exception.ServiceException; +import org.dromara.warm.flow.core.service.DefService; +import org.dromara.warm.flow.core.service.InsService; +import org.dromara.warm.flow.core.service.TaskService; +import org.dromara.warm.flow.orm.entity.FlowInstance; +import org.dromara.warm.flow.orm.mapper.FlowHisTaskMapper; +import org.dromara.warm.flow.orm.mapper.FlowInstanceMapper; +import org.dromara.workflow.mapper.FlwCategoryMapper; +import org.dromara.workflow.mapper.FlwInstanceMapper; +import org.dromara.workflow.service.IFlwTaskService; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +class FlwInstanceProgressTest { + + @Test + void instanceIdAloneLoadsProgress() { + FlowInstanceMapper instanceMapper = mock(FlowInstanceMapper.class); + FlowHisTaskMapper historyMapper = mock(FlowHisTaskMapper.class); + IFlwTaskService taskService = mock(IFlwTaskService.class); + FlwInstanceServiceImpl service = service(instanceMapper, historyMapper, taskService); + FlowInstance instance = mock(FlowInstance.class); + when(instance.getId()).thenReturn(12L); + when(instanceMapper.selectById(12L)).thenReturn(instance); + when(taskService.selectByInstId(12L)).thenReturn(List.of()); + when(historyMapper.selectList(any())).thenReturn(List.of()); + + assertEquals(12L, service.flowHisTaskList(12L, null).get("instanceId")); + verify(instanceMapper).selectById(12L); + } + + @Test + void rejectsMismatchedIdsBeforeLoadingTasks() { + FlowInstanceMapper instanceMapper = mock(FlowInstanceMapper.class); + FlowHisTaskMapper historyMapper = mock(FlowHisTaskMapper.class); + IFlwTaskService taskService = mock(IFlwTaskService.class); + FlwInstanceServiceImpl service = service(instanceMapper, historyMapper, taskService); + FlowInstance instance = mock(FlowInstance.class); + when(instance.getBusinessId()).thenReturn("business-1"); + when(instanceMapper.selectById(12L)).thenReturn(instance); + + assertThrows(ServiceException.class, () -> service.flowHisTaskList(12L, "business-2")); + verifyNoInteractions(taskService, historyMapper); + } + + @Test + void matchingIdsLoadSameInstance() { + FlowInstanceMapper instanceMapper = mock(FlowInstanceMapper.class); + FlowHisTaskMapper historyMapper = mock(FlowHisTaskMapper.class); + IFlwTaskService taskService = mock(IFlwTaskService.class); + FlwInstanceServiceImpl service = service(instanceMapper, historyMapper, taskService); + FlowInstance instance = mock(FlowInstance.class); + when(instance.getId()).thenReturn(12L); + when(instance.getBusinessId()).thenReturn("business-1"); + when(instanceMapper.selectById(12L)).thenReturn(instance); + when(taskService.selectByInstId(12L)).thenReturn(List.of()); + when(historyMapper.selectList(any())).thenReturn(List.of()); + + assertEquals(12L, service.flowHisTaskList(12L, "business-1").get("instanceId")); + verify(instanceMapper).selectById(12L); + } + + private FlwInstanceServiceImpl service(FlowInstanceMapper instanceMapper, + FlowHisTaskMapper historyMapper, IFlwTaskService taskService) { + return new FlwInstanceServiceImpl(mock(InsService.class), mock(DefService.class), + mock(TaskService.class), historyMapper, instanceMapper, taskService, + mock(FlwInstanceMapper.class), mock(FlwCategoryMapper.class)); + } +} diff --git a/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/WorkflowServiceImplEventTest.java b/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/WorkflowServiceImplEventTest.java new file mode 100644 index 000000000..ab1485462 --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/WorkflowServiceImplEventTest.java @@ -0,0 +1,131 @@ +package org.dromara.workflow.service.impl; + +import org.dromara.warm.flow.core.FlowEngine; +import org.dromara.warm.flow.core.config.WarmFlow; +import org.dromara.warm.flow.core.entity.Instance; +import org.dromara.warm.flow.core.service.InsService; +import org.dromara.warm.flow.core.service.TaskService; +import org.dromara.warm.flow.core.workflow.context.WorkflowContext; +import org.dromara.workflow.api.domain.StartMessageEventDTO; +import org.dromara.workflow.api.domain.StartTimerEventDTO; +import org.dromara.workflow.api.domain.TriggerMessageEventDTO; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class WorkflowServiceImplEventTest { + + private final WorkflowServiceImpl service = new WorkflowServiceImpl(null, null); + + @Test + void messageStartPassesPayloadAndHandler() { + StartMessageEventDTO request = new StartMessageEventDTO(); + request.setBusinessId("order-1"); + request.setFlowCode("order_flow"); + request.setEventName("paid"); + request.setPayload(Map.of("amount", 20)); + request.setHandler("42"); + InsService insService = mock(InsService.class); + Instance instance = mock(Instance.class); + when(instance.getId()).thenReturn(100L); + when(insService.startByMessage(eq("order-1"), eq("order_flow"), eq("paid"), eq(request.getPayload()), + any(WorkflowContext.class))).thenReturn(instance); + + try (MockedStatic engine = mockStatic(FlowEngine.class)) { + engine.when(FlowEngine::insService).thenReturn(insService); + assertEquals(100L, service.startByMessageEvent(request)); + ArgumentCaptor context = ArgumentCaptor.forClass(WorkflowContext.class); + verify(insService).startByMessage(eq("order-1"), eq("order_flow"), eq("paid"), + eq(request.getPayload()), context.capture()); + assertEquals("42", context.getValue().getHandler()); + } + } + + @Test + void timerStartGeneratesNumericBusinessIdAndUsesSystemHandler() { + StartTimerEventDTO request = new StartTimerEventDTO(); + request.setDefinitionId(10L); + request.setNodeCode("timerStart"); + InsService insService = mock(InsService.class); + Instance instance = mock(Instance.class); + when(instance.getId()).thenReturn(200L); + when(insService.startByTimer(any(String.class), eq(10L), eq("timerStart"), + any(), any(WorkflowContext.class))).thenReturn(instance); + WarmFlow config = mock(WarmFlow.class); + when(config.getSystemHandler()).thenReturn("system"); + + try (MockedStatic engine = mockStatic(FlowEngine.class)) { + engine.when(FlowEngine::insService).thenReturn(insService); + engine.when(FlowEngine::getFlowConfig).thenReturn(config); + assertEquals(200L, service.startByTimerEvent(request)); + ArgumentCaptor businessId = ArgumentCaptor.forClass(String.class); + ArgumentCaptor context = ArgumentCaptor.forClass(WorkflowContext.class); + verify(insService).startByTimer(businessId.capture(), eq(10L), eq("timerStart"), + any(), context.capture()); + assertTrue(businessId.getValue().matches("[0-9]+")); + assertEquals("system", context.getValue().getHandler()); + } + } + + @Test + void timerStartKeepsProvidedBusinessIdAndHandler() { + StartTimerEventDTO request = new StartTimerEventDTO(); + request.setBusinessId("manual-42"); + request.setDefinitionId(10L); + request.setNodeCode("timerStart"); + request.setHandler("operator"); + request.setPayload(Map.of("reason", "retry")); + InsService insService = mock(InsService.class); + Instance instance = mock(Instance.class); + when(instance.getId()).thenReturn(201L); + when(insService.startByTimer(eq("manual-42"), eq(10L), eq("timerStart"), + eq(request.getPayload()), any(WorkflowContext.class))).thenReturn(instance); + + try (MockedStatic engine = mockStatic(FlowEngine.class)) { + engine.when(FlowEngine::insService).thenReturn(insService); + assertEquals(201L, service.startByTimerEvent(request)); + ArgumentCaptor context = ArgumentCaptor.forClass(WorkflowContext.class); + verify(insService).startByTimer(eq("manual-42"), eq(10L), eq("timerStart"), + eq(request.getPayload()), context.capture()); + assertEquals("operator", context.getValue().getHandler()); + } + } + + @Test + void messageAndErrorEventsDelegateToEngine() { + TriggerMessageEventDTO request = new TriggerMessageEventDTO(); + request.setInstanceId(300L); + request.setEventName("confirmed"); + request.setHandler("42"); + InsService insService = mock(InsService.class); + TaskService taskService = mock(TaskService.class); + Instance instance = mock(Instance.class); + when(instance.getId()).thenReturn(300L); + when(insService.triggerMessage(eq(300L), eq("confirmed"), any(), any(WorkflowContext.class))) + .thenReturn(instance); + when(insService.throwError(300L, "external_error", "failed")).thenReturn(true); + when(taskService.throwError(400L, "external_error", "failed")).thenReturn(true); + + try (MockedStatic engine = mockStatic(FlowEngine.class)) { + engine.when(FlowEngine::insService).thenReturn(insService); + engine.when(FlowEngine::taskService).thenReturn(taskService); + assertEquals(300L, service.triggerMessageEvent(request)); + ArgumentCaptor context = ArgumentCaptor.forClass(WorkflowContext.class); + verify(insService).triggerMessage(eq(300L), eq("confirmed"), any(), context.capture()); + assertEquals("42", context.getValue().getHandler()); + assertTrue(service.throwInstanceErrorEvent(300L, "external_error", "failed")); + assertTrue(service.throwTaskErrorEvent(400L, "external_error", "failed")); + } + } +} diff --git a/script/demo/demo_event_cooldown.json b/script/demo/demo_event_cooldown.json new file mode 100644 index 000000000..e65fa9034 --- /dev/null +++ b/script/demo/demo_event_cooldown.json @@ -0,0 +1,111 @@ +{ + "version": "1", + "category": "1762300000000000100", + "flowCode": "demo_event_cooldown", + "flowName": "账号注销-冷静期示例", + "formPath": "/workflow/leaveEdit/index", + "nodeList": [ + { + "ext": "[]", + "nodeCode": "start", + "nodeName": "开始", + "nodeType": 0, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "85,200;160,200", + "nowNodeCode": "start", + "nextNodeCode": "apply" + } + ], + "nodeRatio": "0", + "coordinate": "60,200|60,200", + "formCustom": "N" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "apply", + "nodeName": "提交注销申请", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "260,200;310,200", + "nowNodeCode": "apply", + "nextNodeCode": "cooldown" + } + ], + "nodeRatio": "0", + "coordinate": "210,200|210,200", + "formCustom": "N", + "permissionFlag": "warmFlowInitiator" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "confirm", + "nodeName": "注销终审确认", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "560,200;610,200", + "nowNodeCode": "confirm", + "nextNodeCode": "msgThrow" + } + ], + "nodeRatio": "0", + "coordinate": "510,200|510,200", + "formCustom": "N", + "permissionFlag": "1761100000000000001" + }, + { + "ext": "[]", + "nodeCode": "cooldown", + "nodeName": "冷静期 2 分钟", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "410,200;460,200", + "nowNodeCode": "cooldown", + "nextNodeCode": "confirm" + } + ], + "eventType": "TIMER_CATCH", + "nodeRatio": "0", + "coordinate": "360,200|360,200", + "formCustom": "N", + "nodeConfig": "{\"event\":{\"timerType\":\"AFTER\",\"timerValue\":\"PT2M\"}}" + }, + { + "ext": "[]", + "nodeCode": "msgThrow", + "nodeName": "注销结果推送", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "710,200;755,200", + "nowNodeCode": "msgThrow", + "nextNodeCode": "end" + } + ], + "eventName": "membership.cancelled", + "eventType": "MESSAGE_THROW", + "nodeRatio": "0", + "coordinate": "660,200|660,200", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "end", + "nodeName": "结束", + "nodeType": 2, + "nodeRatio": "0", + "coordinate": "780,200|780,200", + "formCustom": "N" + } + ], + "formCustom": "N", + "modelValue": "CLASSICS" +} diff --git a/script/demo/demo_event_daily_check.json b/script/demo/demo_event_daily_check.json new file mode 100644 index 000000000..75c24f50f --- /dev/null +++ b/script/demo/demo_event_daily_check.json @@ -0,0 +1,194 @@ +{ + "version": "2", + "category": "1762300000000000100", + "flowCode": "demo_event_daily_check", + "flowName": "每日自动巡检-终止结束示例", + "formPath": "/workflow/leaveEdit/index", + "nodeList": [ + { + "ext": "[]", + "nodeCode": "start", + "nodeName": "开始", + "nodeType": 0, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "88,260;190,260", + "nowNodeCode": "start", + "nextNodeCode": "timerStart" + } + ], + "nodeRatio": "0", + "coordinate": "60,260|60,260", + "formCustom": "N" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "inspect", + "nodeName": "执行数据巡检", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "470,260;555,260", + "nowNodeCode": "inspect", + "nextNodeCode": "gw1" + } + ], + "nodeRatio": "0", + "coordinate": "420,260|420,260", + "formCustom": "N", + "permissionFlag": "1761100000000000001" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "qa", + "nodeName": "质检复核", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "830,120;1020,120;1020,235", + "nowNodeCode": "qa", + "nextNodeCode": "gw2" + } + ], + "nodeRatio": "0", + "coordinate": "780,120|780,120", + "formCustom": "N", + "permissionFlag": "1761100000000000003" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "risk", + "nodeName": "风控否决判定", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "830,400;870,400", + "nowNodeCode": "risk", + "nextNodeCode": "gwRisk" + } + ], + "nodeRatio": "0", + "coordinate": "780,400|780,400", + "formCustom": "N", + "permissionFlag": "1761100000000000004" + }, + { + "ext": "[]", + "nodeCode": "terminate", + "nodeName": "严重异常终止", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "1070,400;1220,400;1220,288", + "nowNodeCode": "terminate", + "nextNodeCode": "end" + } + ], + "eventType": "TERMINATE_END", + "nodeRatio": "0", + "coordinate": "1020,400|1020,400", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "timerStart", + "nodeName": "每天 09:00 自动发起", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "290,260;370,260", + "nowNodeCode": "timerStart", + "nextNodeCode": "inspect" + } + ], + "eventType": "TIMER_START", + "nodeRatio": "0", + "coordinate": "240,260|240,260", + "formCustom": "N", + "nodeConfig": "{\"event\": {\"timerType\": \"POINT_IN_TIME\", \"timerValue\": \"[\\\"09:00\\\"]\", \"timezone\": \"Asia/Shanghai\", \"variables\": {\"veto\": 0, \"channel\": \"daily\"}}}" + }, + { + "ext": "[]", + "nodeCode": "end", + "nodeName": "结束", + "nodeType": 2, + "nodeRatio": "0", + "coordinate": "1220,260|1220,260", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "gwRisk", + "nodeName": "是否严重异常", + "nodeType": 3, + "skipList": [ + { + "skipName": "严重异常", + "skipType": "PASS", + "coordinate": "925,400;970,400", + "nowNodeCode": "gwRisk", + "nextNodeCode": "terminate", + "skipCondition": "eq@@veto|1" + }, + { + "skipName": "无异常", + "skipType": "PASS", + "coordinate": "900,378;900,260;995,260", + "nowNodeCode": "gwRisk", + "nextNodeCode": "gw2" + } + ], + "nodeRatio": "0", + "coordinate": "900,400", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "gw1", + "nodeName": "并行分支", + "nodeType": 4, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "605,260;700,260;700,120;730,120", + "nowNodeCode": "gw1", + "nextNodeCode": "qa" + }, + { + "skipType": "PASS", + "coordinate": "605,260;700,260;700,400;730,400", + "nowNodeCode": "gw1", + "nextNodeCode": "risk" + } + ], + "nodeRatio": "0", + "coordinate": "580,260", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "gw2", + "nodeName": "汇聚", + "nodeType": 4, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "1045,260;1192,260", + "nowNodeCode": "gw2", + "nextNodeCode": "end" + } + ], + "nodeRatio": "0", + "coordinate": "1020,260", + "formCustom": "N" + } + ], + "formCustom": "N", + "modelValue": "CLASSICS" +} diff --git a/script/demo/demo_event_notify.json b/script/demo/demo_event_notify.json new file mode 100644 index 000000000..846ce9261 --- /dev/null +++ b/script/demo/demo_event_notify.json @@ -0,0 +1,74 @@ +{ + "version": "1", + "category": "1762300000000000100", + "flowCode": "demo_event_notify", + "flowName": "注销结果通知-消息启动示例", + "formPath": "/workflow/leaveEdit/index", + "nodeList": [ + { + "ext": "[]", + "nodeCode": "start", + "nodeName": "开始", + "nodeType": 0, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "85,200;160,200", + "nowNodeCode": "start", + "nextNodeCode": "msgStart" + } + ], + "nodeRatio": "0", + "coordinate": "60,200|60,200", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "msgStart", + "nodeName": "账号已注销", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "260,200;310,200", + "nowNodeCode": "msgStart", + "nextNodeCode": "notify" + } + ], + "eventName": "membership.cancelled", + "eventType": "MESSAGE_START", + "nodeRatio": "0", + "coordinate": "210,200|210,200", + "formCustom": "N" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "notify", + "nodeName": "发送注销通知", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "410,200;455,200", + "nowNodeCode": "notify", + "nextNodeCode": "end" + } + ], + "nodeRatio": "0", + "coordinate": "360,200|360,200", + "formCustom": "N", + "permissionFlag": "1761100000000000001" + }, + { + "ext": "[]", + "nodeCode": "end", + "nodeName": "结束", + "nodeType": 2, + "nodeRatio": "0", + "coordinate": "480,200|480,200", + "formCustom": "N" + } + ], + "formCustom": "N", + "modelValue": "CLASSICS" +} diff --git a/script/demo/demo_event_pay_fail.json b/script/demo/demo_event_pay_fail.json new file mode 100644 index 000000000..b900a5dad --- /dev/null +++ b/script/demo/demo_event_pay_fail.json @@ -0,0 +1,120 @@ +{ + "version": "1", + "category": "1762300000000000100", + "flowCode": "demo_event_pay_fail", + "flowName": "付款失败善后示例", + "formPath": "/workflow/leaveEdit/index", + "nodeList": [ + { + "ext": "[]", + "nodeCode": "start", + "nodeName": "开始", + "nodeType": 0, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "85,200;160,200", + "nowNodeCode": "start", + "nextNodeCode": "apply" + } + ], + "nodeRatio": "0", + "coordinate": "60,200|60,200", + "formCustom": "N" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "apply", + "nodeName": "发起付款申请", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "260,200;310,200", + "nowNodeCode": "apply", + "nextNodeCode": "pay" + } + ], + "nodeRatio": "0", + "coordinate": "210,200|210,200", + "formCustom": "N", + "permissionFlag": "warmFlowInitiator" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "pay", + "nodeName": "财务执行付款", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "410,200;755,200", + "nowNodeCode": "pay", + "nextNodeCode": "end" + }, + { + "skipType": "PASS", + "coordinate": "360,240;360,360", + "nowNodeCode": "pay", + "nextNodeCode": "payBoundary" + } + ], + "nodeRatio": "0", + "coordinate": "360,200|360,200", + "formCustom": "N", + "listenerPath": "org.dromara.workflow.demo.DemoPayFailListener", + "listenerType": "create", + "permissionFlag": "1761100000000000004" + }, + { + "ext": "[]", + "nodeCode": "payBoundary", + "nodeName": "付款失败善后", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "410,400;460,400", + "nowNodeCode": "payBoundary", + "nextNodeCode": "rescue" + } + ], + "eventName": "PAY_FAILED", + "eventType": "ERROR_BOUNDARY", + "nodeRatio": "0", + "coordinate": "360,400|360,400", + "formCustom": "N", + "interrupting": 1, + "attachedToNodeCode": "pay" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "rescue", + "nodeName": "失败善后处理", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "560,400;780,400;780,225", + "nowNodeCode": "rescue", + "nextNodeCode": "end" + } + ], + "nodeRatio": "0", + "coordinate": "510,400|510,400", + "formCustom": "N", + "permissionFlag": "1761100000000000001" + }, + { + "ext": "[]", + "nodeCode": "end", + "nodeName": "结束", + "nodeType": 2, + "nodeRatio": "0", + "coordinate": "780,200|780,200", + "formCustom": "N" + } + ], + "formCustom": "N", + "modelValue": "CLASSICS" +} diff --git a/script/demo/demo_event_refund.json b/script/demo/demo_event_refund.json new file mode 100644 index 000000000..ad52b9b3e --- /dev/null +++ b/script/demo/demo_event_refund.json @@ -0,0 +1,149 @@ +{ + "version": "1", + "category": "1762300000000000100", + "flowCode": "demo_event_refund", + "flowName": "退款审批-超时升级示例", + "formPath": "/workflow/leaveEdit/index", + "nodeList": [ + { + "ext": "[]", + "nodeCode": "start", + "nodeName": "开始", + "nodeType": 0, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "85,200;160,200", + "nowNodeCode": "start", + "nextNodeCode": "apply" + } + ], + "nodeRatio": "0", + "coordinate": "60,200|60,200", + "formCustom": "N" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "apply", + "nodeName": "提交退款申请", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "260,200;310,200", + "nowNodeCode": "apply", + "nextNodeCode": "review" + } + ], + "nodeRatio": "0", + "coordinate": "210,200|210,200", + "formCustom": "N", + "permissionFlag": "warmFlowInitiator" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "finance", + "nodeName": "财务退款", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "710,200;760,200", + "nowNodeCode": "finance", + "nextNodeCode": "msgEnd" + } + ], + "nodeRatio": "0", + "coordinate": "660,200|660,200", + "formCustom": "N", + "permissionFlag": "1761100000000000001" + }, + { + "ext": "[]", + "nodeCode": "msgEnd", + "nodeName": "退款结果通知", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "860,200;905,200", + "nowNodeCode": "msgEnd", + "nextNodeCode": "end" + } + ], + "eventName": "refund.finish", + "eventType": "MESSAGE_END", + "nodeRatio": "0", + "coordinate": "810,200|810,200", + "formCustom": "N" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "review", + "nodeName": "客服初审", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "360,240;360,400;460,400", + "nowNodeCode": "review", + "nextNodeCode": "supervisor" + } + ], + "nodeRatio": "0", + "coordinate": "360,200|360,200", + "formCustom": "N", + "permissionFlag": "1761100000000000004" + }, + { + "ext": "[]", + "nodeCode": "reviewBoundary", + "nodeName": "初审超时升级", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "410,400;460,400", + "nowNodeCode": "reviewBoundary", + "nextNodeCode": "supervisor" + } + ], + "eventType": "TIMER_BOUNDARY", + "nodeRatio": "0", + "coordinate": "360,400|360,400", + "formCustom": "N", + "nodeConfig": "{\"event\":{\"timerType\":\"AFTER\",\"timerValue\":\"PT2M\"}}", + "interrupting": 1, + "attachedToNodeCode": "review" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "supervisor", + "nodeName": "客服主管复核", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "560,400;660,400;660,240", + "nowNodeCode": "supervisor", + "nextNodeCode": "finance" + } + ], + "nodeRatio": "0", + "coordinate": "510,400|510,400", + "formCustom": "N", + "permissionFlag": "1761100000000000001" + }, + { + "ext": "[]", + "nodeCode": "end", + "nodeName": "结束", + "nodeType": 2, + "nodeRatio": "0", + "coordinate": "930,200|930,200", + "formCustom": "N" + } + ], + "formCustom": "N", + "modelValue": "CLASSICS" +} diff --git a/script/demo/demo_event_risk_wait.json b/script/demo/demo_event_risk_wait.json new file mode 100644 index 000000000..450085c3e --- /dev/null +++ b/script/demo/demo_event_risk_wait.json @@ -0,0 +1,93 @@ +{ + "version": "1", + "category": "1762300000000000100", + "flowCode": "demo_event_risk_wait", + "flowName": "支付对账-等待外部回调示例", + "formPath": "/workflow/leaveEdit/index", + "nodeList": [ + { + "ext": "[]", + "nodeCode": "start", + "nodeName": "开始", + "nodeType": 0, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "85,200;160,200", + "nowNodeCode": "start", + "nextNodeCode": "msgStart" + } + ], + "nodeRatio": "0", + "coordinate": "60,200|60,200", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "msgStart", + "nodeName": "订单已支付", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "260,200;310,200", + "nowNodeCode": "msgStart", + "nextNodeCode": "riskCatch" + } + ], + "eventName": "order.paid", + "eventType": "MESSAGE_START", + "nodeRatio": "0", + "coordinate": "210,200|210,200", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "riskCatch", + "nodeName": "等待风控审核结果", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "410,200;460,200", + "nowNodeCode": "riskCatch", + "nextNodeCode": "settle" + } + ], + "eventName": "risk.result", + "eventType": "MESSAGE_CATCH", + "nodeRatio": "0", + "coordinate": "360,200|360,200", + "formCustom": "N" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "settle", + "nodeName": "对账确认", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "560,200;605,200", + "nowNodeCode": "settle", + "nextNodeCode": "end" + } + ], + "nodeRatio": "0", + "coordinate": "510,200|510,200", + "formCustom": "N", + "permissionFlag": "1761100000000000001" + }, + { + "ext": "[]", + "nodeCode": "end", + "nodeName": "结束", + "nodeType": 2, + "nodeRatio": "0", + "coordinate": "630,200|630,200", + "formCustom": "N" + } + ], + "formCustom": "N", + "modelValue": "CLASSICS" +} diff --git a/script/demo/demo_gateway_classic.json b/script/demo/demo_gateway_classic.json new file mode 100644 index 000000000..4ceb28388 --- /dev/null +++ b/script/demo/demo_gateway_classic.json @@ -0,0 +1,385 @@ +{ + "version": "1", + "category": "1762300000000000100", + "flowCode": "demo_gateway_classic", + "flowName": "网关综合-经典模式示例", + "formPath": "/workflow/leaveEdit/index", + "nodeList": [ + { + "ext": "[]", + "nodeCode": "start", + "nodeName": "开始", + "nodeType": 0, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "88,260;180,260", + "nowNodeCode": "start", + "nextNodeCode": "submit" + } + ], + "nodeRatio": "0", + "coordinate": "60,260|60,260", + "formCustom": "N" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file,subSign,addSign,transfer,trust\"}]", + "nodeCode": "51ca4cb3-684b-4c3d-b474-9ec3363440bc", + "nodeName": "第四个", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "860,140;935,140;935,260;975,260", + "nowNodeCode": "51ca4cb3-684b-4c3d-b474-9ec3363440bc", + "nextNodeCode": "17000bff-87ab-4b6d-babc-0f3c51b48858" + } + ], + "nodeRatio": "100", + "coordinate": "810,140|810,140", + "formCustom": "N", + "permissionFlag": "1761100000000000001@@1761100000000000003" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "54e21807-9501-4e49-90b9-1104f359709e", + "nodeName": "包容小于三", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "2180,200;2280,200;2280,260;2305,260", + "nowNodeCode": "54e21807-9501-4e49-90b9-1104f359709e", + "nextNodeCode": "ed4a73f7-5197-4362-a4c7-e6295ad211c4" + } + ], + "nodeRatio": "0", + "coordinate": "2130,200|2130,200", + "formCustom": "N", + "permissionFlag": "1761100000000000001" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "79a20053-b41e-4f16-987d-5ac6e2a37a65", + "nodeName": "包容4", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "2180,500;2280,500;2280,260;2305,260", + "nowNodeCode": "79a20053-b41e-4f16-987d-5ac6e2a37a65", + "nextNodeCode": "ed4a73f7-5197-4362-a4c7-e6295ad211c4" + } + ], + "nodeRatio": "0", + "coordinate": "2130,500|2130,500", + "formCustom": "N", + "permissionFlag": "1761100000000000004" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "86d7c888-0428-4c0f-b462-6e4563c21daf", + "nodeName": "并行一", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "1420,140;1530,140", + "nowNodeCode": "86d7c888-0428-4c0f-b462-6e4563c21daf", + "nextNodeCode": "8d4baa8d-b2ca-4de0-96df-33e46aa12bb4" + } + ], + "nodeRatio": "0", + "coordinate": "1370,140|1370,140", + "formCustom": "N", + "permissionFlag": "1761100000000000001" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "8b46aefa-c496-48c4-ae2c-381035f69dda", + "nodeName": "最后一个", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "2560,260;2652,260", + "nowNodeCode": "8b46aefa-c496-48c4-ae2c-381035f69dda", + "nextNodeCode": "19c75cd4-36c3-48f7-96ad-9205f9337a2f" + } + ], + "nodeRatio": "0", + "coordinate": "2510,260|2510,260", + "formCustom": "N", + "permissionFlag": "1761100000000000001" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file,subSign,addSign\"}]", + "nodeCode": "8d4baa8d-b2ca-4de0-96df-33e46aa12bb4", + "nodeName": "并行二", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "1630,140;1700,140;1700,260;1735,260", + "nowNodeCode": "8d4baa8d-b2ca-4de0-96df-33e46aa12bb4", + "nextNodeCode": "ca78e892-b181-4d2e-afbe-21e0d1936fe5" + } + ], + "nodeRatio": "60", + "coordinate": "1580,140|1580,140", + "formCustom": "N", + "permissionFlag": "1761100000000000001@@1761100000000000003@@1761100000000000004" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file,transfer,trust,addSign,subSign\"}]", + "nodeCode": "974a0d43-53a1-4384-8ee1-2f9665d5aa2d", + "nodeName": "大于三天", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "640,140;760,140", + "nowNodeCode": "974a0d43-53a1-4384-8ee1-2f9665d5aa2d", + "nextNodeCode": "51ca4cb3-684b-4c3d-b474-9ec3363440bc" + } + ], + "nodeRatio": "0", + "coordinate": "590,140|590,140", + "formCustom": "N", + "permissionFlag": "1761100000000000003" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "b057d0fb-068d-47db-ae9e-8f4356600672", + "nodeName": "包容3", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "2180,350;2255,350;2255,260;2305,260", + "nowNodeCode": "b057d0fb-068d-47db-ae9e-8f4356600672", + "nextNodeCode": "ed4a73f7-5197-4362-a4c7-e6295ad211c4" + } + ], + "nodeRatio": "0", + "coordinate": "2130,350|2130,350", + "formCustom": "N", + "permissionFlag": "1761100000000000001" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file,subSign,transfer,addSign,trust\"}]", + "nodeCode": "dc06e24f-b67c-4268-b2d7-dea1941ef462", + "nodeName": "小于三天", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "640,380;935,380;935,260;975,260", + "nowNodeCode": "dc06e24f-b67c-4268-b2d7-dea1941ef462", + "nextNodeCode": "17000bff-87ab-4b6d-babc-0f3c51b48858" + } + ], + "nodeRatio": "0", + "coordinate": "590,380|590,380", + "formCustom": "N", + "permissionFlag": "1761100000000000001" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "f3482139-ab8e-4f18-940a-d3298346caff", + "nodeName": "包容day大于三", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "2180,50;2255,50;2255,260;2305,260", + "nowNodeCode": "f3482139-ab8e-4f18-940a-d3298346caff", + "nextNodeCode": "ed4a73f7-5197-4362-a4c7-e6295ad211c4" + } + ], + "nodeRatio": "0", + "coordinate": "2130,50|2130,50", + "formCustom": "N", + "permissionFlag": "1761100000000000003" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "f6fc6c06-4d64-4ee6-8f6a-4c5212d01ccc", + "nodeName": "并行一同级", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "1420,400;1700,400;1700,260;1735,260", + "nowNodeCode": "f6fc6c06-4d64-4ee6-8f6a-4c5212d01ccc", + "nextNodeCode": "ca78e892-b181-4d2e-afbe-21e0d1936fe5" + } + ], + "nodeRatio": "0", + "coordinate": "1370,400|1370,400", + "formCustom": "N", + "permissionFlag": "1761100000000000001" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "submit", + "nodeName": "申请人", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "280,260;365,260", + "nowNodeCode": "submit", + "nextNodeCode": "e9b6ac64-5deb-4af9-81f2-4b750b8b890a" + } + ], + "nodeRatio": "0", + "coordinate": "230,260|230,260", + "formCustom": "N", + "permissionFlag": "warmFlowInitiator" + }, + { + "ext": "[]", + "nodeCode": "19c75cd4-36c3-48f7-96ad-9205f9337a2f", + "nodeName": "结束", + "nodeType": 2, + "nodeRatio": "0", + "coordinate": "2680,260|2680,260", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "17000bff-87ab-4b6d-babc-0f3c51b48858", + "nodeType": 3, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "1025,260;1145,260", + "nowNodeCode": "17000bff-87ab-4b6d-babc-0f3c51b48858", + "nextNodeCode": "ffa7942a-e0b4-42c8-9aa6-83ebb6208547" + } + ], + "nodeRatio": "0", + "coordinate": "1000,260", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "e9b6ac64-5deb-4af9-81f2-4b750b8b890a", + "nodeType": 3, + "skipList": [ + { + "skipName": "day>3", + "skipType": "PASS", + "coordinate": "415,260;480,260;480,140;540,140", + "nowNodeCode": "e9b6ac64-5deb-4af9-81f2-4b750b8b890a", + "nextNodeCode": "974a0d43-53a1-4384-8ee1-2f9665d5aa2d", + "skipCondition": "gt@@day|3" + }, + { + "skipType": "PASS", + "coordinate": "415,260;480,260;480,380;540,380", + "nowNodeCode": "e9b6ac64-5deb-4af9-81f2-4b750b8b890a", + "nextNodeCode": "dc06e24f-b67c-4268-b2d7-dea1941ef462" + } + ], + "nodeRatio": "0", + "coordinate": "390,260", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "ca78e892-b181-4d2e-afbe-21e0d1936fe5", + "nodeType": 4, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "1785,260;1905,260", + "nowNodeCode": "ca78e892-b181-4d2e-afbe-21e0d1936fe5", + "nextNodeCode": "761c8e6e-169d-48f7-be85-1b3f1c3c1646" + } + ], + "nodeRatio": "0", + "coordinate": "1760,260", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "ffa7942a-e0b4-42c8-9aa6-83ebb6208547", + "nodeType": 4, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "1195,260;1265,260;1265,140;1320,140", + "nowNodeCode": "ffa7942a-e0b4-42c8-9aa6-83ebb6208547", + "nextNodeCode": "86d7c888-0428-4c0f-b462-6e4563c21daf" + }, + { + "skipType": "PASS", + "coordinate": "1195,260;1265,260;1265,400;1320,400", + "nowNodeCode": "ffa7942a-e0b4-42c8-9aa6-83ebb6208547", + "nextNodeCode": "f6fc6c06-4d64-4ee6-8f6a-4c5212d01ccc" + } + ], + "nodeRatio": "0", + "coordinate": "1170,260", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "761c8e6e-169d-48f7-be85-1b3f1c3c1646", + "nodeType": 5, + "skipList": [ + { + "skipName": "day>3", + "skipType": "PASS", + "coordinate": "1955,260;2020,260;2020,50;2080,50", + "nowNodeCode": "761c8e6e-169d-48f7-be85-1b3f1c3c1646", + "nextNodeCode": "f3482139-ab8e-4f18-940a-d3298346caff", + "skipCondition": "gt@@day|3" + }, + { + "skipType": "PASS", + "coordinate": "1955,260;2020,260;2020,350;2080,350", + "nowNodeCode": "761c8e6e-169d-48f7-be85-1b3f1c3c1646", + "nextNodeCode": "b057d0fb-068d-47db-ae9e-8f4356600672" + }, + { + "skipType": "PASS", + "coordinate": "1955,260;2045,260;2045,500;2080,500", + "nowNodeCode": "761c8e6e-169d-48f7-be85-1b3f1c3c1646", + "nextNodeCode": "79a20053-b41e-4f16-987d-5ac6e2a37a65" + }, + { + "skipName": "day<3", + "skipType": "PASS", + "coordinate": "1955,260;2045,260;2045,200;2080,200", + "nowNodeCode": "761c8e6e-169d-48f7-be85-1b3f1c3c1646", + "nextNodeCode": "54e21807-9501-4e49-90b9-1104f359709e", + "skipCondition": "lt@@day|3" + } + ], + "nodeRatio": "0", + "coordinate": "1930,260", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "ed4a73f7-5197-4362-a4c7-e6295ad211c4", + "nodeType": 5, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "2355,260;2460,260", + "nowNodeCode": "ed4a73f7-5197-4362-a4c7-e6295ad211c4", + "nextNodeCode": "8b46aefa-c496-48c4-ae2c-381035f69dda" + } + ], + "nodeRatio": "0", + "coordinate": "2330,260", + "formCustom": "N" + } + ], + "formCustom": "N", + "modelValue": "CLASSICS" +} diff --git a/script/demo/demo_gateway_mimic.json b/script/demo/demo_gateway_mimic.json new file mode 100644 index 000000000..8dd82b727 --- /dev/null +++ b/script/demo/demo_gateway_mimic.json @@ -0,0 +1,385 @@ +{ + "version": "1", + "category": "1762300000000000100", + "flowCode": "demo_gateway_mimic", + "flowName": "网关综合-仿钉钉模式示例", + "formPath": "/workflow/leaveEdit/index", + "nodeList": [ + { + "ext": "[]", + "nodeCode": "start", + "nodeName": "开始", + "nodeType": 0, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "700,120;700,202", + "nowNodeCode": "start", + "nextNodeCode": "submit" + } + ], + "nodeRatio": "0", + "coordinate": "700,100|700,100", + "formCustom": "N" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file,subSign,addSign,transfer,trust\"}]", + "nodeCode": "51ca4cb3-684b-4c3d-b474-9ec3363440bc", + "nodeName": "第四个", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "550,698;550,782;682,800", + "nowNodeCode": "51ca4cb3-684b-4c3d-b474-9ec3363440bc", + "nextNodeCode": "17000bff-87ab-4b6d-babc-0f3c51b48858" + } + ], + "nodeRatio": "100", + "coordinate": "550,660|550,660", + "formCustom": "N", + "permissionFlag": "1761100000000000001@@1761100000000000003" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "54e21807-9501-4e49-90b9-1104f359709e", + "nodeName": "包容小于三", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "1150,1648;1150,1740;718,1740", + "nowNodeCode": "54e21807-9501-4e49-90b9-1104f359709e", + "nextNodeCode": "ed4a73f7-5197-4362-a4c7-e6295ad211c4" + } + ], + "nodeRatio": "0", + "coordinate": "1150,1610|1150,1610", + "formCustom": "N", + "permissionFlag": "1761100000000000001" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "79a20053-b41e-4f16-987d-5ac6e2a37a65", + "nodeName": "包容4", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "850,1648;850,1740;718,1740", + "nowNodeCode": "79a20053-b41e-4f16-987d-5ac6e2a37a65", + "nextNodeCode": "ed4a73f7-5197-4362-a4c7-e6295ad211c4" + } + ], + "nodeRatio": "0", + "coordinate": "850,1610|850,1610", + "formCustom": "N", + "permissionFlag": "1761100000000000004" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "86d7c888-0428-4c0f-b462-6e4563c21daf", + "nodeName": "并行一", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "550,1108;550,1172", + "nowNodeCode": "86d7c888-0428-4c0f-b462-6e4563c21daf", + "nextNodeCode": "8d4baa8d-b2ca-4de0-96df-33e46aa12bb4" + } + ], + "nodeRatio": "0", + "coordinate": "550,1070|550,1070", + "formCustom": "N", + "permissionFlag": "1761100000000000001" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "8b46aefa-c496-48c4-ae2c-381035f69dda", + "nodeName": "最后一个", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "700,1908;700,1990", + "nowNodeCode": "8b46aefa-c496-48c4-ae2c-381035f69dda", + "nextNodeCode": "19c75cd4-36c3-48f7-96ad-9205f9337a2f" + } + ], + "nodeRatio": "0", + "coordinate": "700,1870|700,1870", + "formCustom": "N", + "permissionFlag": "1761100000000000001" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file,subSign,addSign\"}]", + "nodeCode": "8d4baa8d-b2ca-4de0-96df-33e46aa12bb4", + "nodeName": "并行二", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "550,1248;550,1340;682,1340", + "nowNodeCode": "8d4baa8d-b2ca-4de0-96df-33e46aa12bb4", + "nextNodeCode": "ca78e892-b181-4d2e-afbe-21e0d1936fe5" + } + ], + "nodeRatio": "60", + "coordinate": "550,1210|550,1210", + "formCustom": "N", + "permissionFlag": "1761100000000000001@@1761100000000000003@@1761100000000000004" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file,transfer,trust,addSign,subSign\"}]", + "nodeCode": "974a0d43-53a1-4384-8ee1-2f9665d5aa2d", + "nodeName": "大于三天", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "550,558;550,622", + "nowNodeCode": "974a0d43-53a1-4384-8ee1-2f9665d5aa2d", + "nextNodeCode": "51ca4cb3-684b-4c3d-b474-9ec3363440bc" + } + ], + "nodeRatio": "0", + "coordinate": "550,520|550,520", + "formCustom": "N", + "permissionFlag": "1761100000000000003" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "b057d0fb-068d-47db-ae9e-8f4356600672", + "nodeName": "包容3", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "550,1648;550,1740;682,1740", + "nowNodeCode": "b057d0fb-068d-47db-ae9e-8f4356600672", + "nextNodeCode": "ed4a73f7-5197-4362-a4c7-e6295ad211c4" + } + ], + "nodeRatio": "0", + "coordinate": "550,1610|550,1610", + "formCustom": "N", + "permissionFlag": "1761100000000000001" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file,subSign,transfer,addSign,trust\"}]", + "nodeCode": "dc06e24f-b67c-4268-b2d7-dea1941ef462", + "nodeName": "小于三天", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "850,558;850,800;718,800", + "nowNodeCode": "dc06e24f-b67c-4268-b2d7-dea1941ef462", + "nextNodeCode": "17000bff-87ab-4b6d-babc-0f3c51b48858" + } + ], + "nodeRatio": "0", + "coordinate": "850,520|850,520", + "formCustom": "N", + "permissionFlag": "1761100000000000001" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "f3482139-ab8e-4f18-940a-d3298346caff", + "nodeName": "包容day大于三", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "250,1648;250,1740;682,1740", + "nowNodeCode": "f3482139-ab8e-4f18-940a-d3298346caff", + "nextNodeCode": "ed4a73f7-5197-4362-a4c7-e6295ad211c4" + } + ], + "nodeRatio": "0", + "coordinate": "250,1610|250,1610", + "formCustom": "N", + "permissionFlag": "1761100000000000003" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "f6fc6c06-4d64-4ee6-8f6a-4c5212d01ccc", + "nodeName": "并行一同级", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "850,1108;850,1340;718,1340", + "nowNodeCode": "f6fc6c06-4d64-4ee6-8f6a-4c5212d01ccc", + "nextNodeCode": "ca78e892-b181-4d2e-afbe-21e0d1936fe5" + } + ], + "nodeRatio": "0", + "coordinate": "850,1070|850,1070", + "formCustom": "N", + "permissionFlag": "1761100000000000001" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "submit", + "nodeName": "申请人", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "700,278;700,362", + "nowNodeCode": "submit", + "nextNodeCode": "e9b6ac64-5deb-4af9-81f2-4b750b8b890a" + } + ], + "nodeRatio": "0", + "coordinate": "700,240|700,240", + "formCustom": "N", + "permissionFlag": "warmFlowInitiator" + }, + { + "ext": "[]", + "nodeCode": "19c75cd4-36c3-48f7-96ad-9205f9337a2f", + "nodeName": "结束", + "nodeType": 2, + "nodeRatio": "0", + "coordinate": "700,2010|700,2010", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "17000bff-87ab-4b6d-babc-0f3c51b48858", + "nodeType": 3, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "700,818;700,912", + "nowNodeCode": "17000bff-87ab-4b6d-babc-0f3c51b48858", + "nextNodeCode": "ffa7942a-e0b4-42c8-9aa6-83ebb6208547" + } + ], + "nodeRatio": "0", + "coordinate": "700,800|700,800", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "e9b6ac64-5deb-4af9-81f2-4b750b8b890a", + "nodeType": 3, + "skipList": [ + { + "skipName": "day>3", + "skipType": "PASS", + "coordinate": "682,380;550,380;550,482|550,410", + "nowNodeCode": "e9b6ac64-5deb-4af9-81f2-4b750b8b890a", + "nextNodeCode": "974a0d43-53a1-4384-8ee1-2f9665d5aa2d", + "skipCondition": "gt@@day|3" + }, + { + "skipType": "PASS", + "coordinate": "718,380;850,380;850,482", + "nowNodeCode": "e9b6ac64-5deb-4af9-81f2-4b750b8b890a", + "nextNodeCode": "dc06e24f-b67c-4268-b2d7-dea1941ef462" + } + ], + "nodeRatio": "0", + "coordinate": "700,380|700,380", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "ca78e892-b181-4d2e-afbe-21e0d1936fe5", + "nodeType": 4, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "700,1358;700,1452", + "nowNodeCode": "ca78e892-b181-4d2e-afbe-21e0d1936fe5", + "nextNodeCode": "761c8e6e-169d-48f7-be85-1b3f1c3c1646" + } + ], + "nodeRatio": "0", + "coordinate": "700,1340|700,1340", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "ffa7942a-e0b4-42c8-9aa6-83ebb6208547", + "nodeType": 4, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "682,930;550,930;550,1032", + "nowNodeCode": "ffa7942a-e0b4-42c8-9aa6-83ebb6208547", + "nextNodeCode": "86d7c888-0428-4c0f-b462-6e4563c21daf" + }, + { + "skipType": "PASS", + "coordinate": "718,930;850,930;850,1032", + "nowNodeCode": "ffa7942a-e0b4-42c8-9aa6-83ebb6208547", + "nextNodeCode": "f6fc6c06-4d64-4ee6-8f6a-4c5212d01ccc" + } + ], + "nodeRatio": "0", + "coordinate": "700,930|700,930", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "761c8e6e-169d-48f7-be85-1b3f1c3c1646", + "nodeType": 5, + "skipList": [ + { + "skipName": "day>3", + "skipType": "PASS", + "coordinate": "682,1470;250,1470;250,1572|250,1500", + "nowNodeCode": "761c8e6e-169d-48f7-be85-1b3f1c3c1646", + "nextNodeCode": "f3482139-ab8e-4f18-940a-d3298346caff", + "skipCondition": "gt@@day|3" + }, + { + "skipType": "PASS", + "coordinate": "682,1470;550,1470;550,1572", + "nowNodeCode": "761c8e6e-169d-48f7-be85-1b3f1c3c1646", + "nextNodeCode": "b057d0fb-068d-47db-ae9e-8f4356600672" + }, + { + "skipType": "PASS", + "coordinate": "718,1470;850,1470;850,1572", + "nowNodeCode": "761c8e6e-169d-48f7-be85-1b3f1c3c1646", + "nextNodeCode": "79a20053-b41e-4f16-987d-5ac6e2a37a65" + }, + { + "skipName": "day<3", + "skipType": "PASS", + "coordinate": "718,1470;1150,1470;1150,1572|1150,1500", + "nowNodeCode": "761c8e6e-169d-48f7-be85-1b3f1c3c1646", + "nextNodeCode": "54e21807-9501-4e49-90b9-1104f359709e", + "skipCondition": "lt@@day|3" + } + ], + "nodeRatio": "0", + "coordinate": "700,1470|700,1470", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "ed4a73f7-5197-4362-a4c7-e6295ad211c4", + "nodeType": 5, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "700,1758;700,1832", + "nowNodeCode": "ed4a73f7-5197-4362-a4c7-e6295ad211c4", + "nextNodeCode": "8b46aefa-c496-48c4-ae2c-381035f69dda" + } + ], + "nodeRatio": "0", + "coordinate": "700,1740|700,1740", + "formCustom": "N" + } + ], + "formCustom": "N", + "modelValue": "MIMIC" +} diff --git a/script/leave/leave1.json b/script/demo/demo_leave1.json similarity index 56% rename from script/leave/leave1.json rename to script/demo/demo_leave1.json index 9abf1c7d7..0e13f995f 100644 --- a/script/leave/leave1.json +++ b/script/demo/demo_leave1.json @@ -1,129 +1,91 @@ { + "version": "1", + "category": "1762300000000000103", + "flowCode": "demo_leave1", + "flowName": "请假申请-普通", + "formPath": "/workflow/leaveEdit/index", "nodeList": [ { - "nodeType": "0", + "ext": "[]", "nodeCode": "d5ee3ddf-3968-4379-a86f-9ceabde5faac", "nodeName": "开始", - "permissionFlag": null, - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": null, - "listenerPath": null, - "formCustom": "N", - "formPath": null, - "ext": "[]", - "coordinate": "200,200|200,200", - "version": "1", + "nodeType": 0, "skipList": [ { "skipType": "PASS", - "skipCondition": null, - "skipName": null, + "coordinate": "220,200;310,200", "nowNodeCode": "d5ee3ddf-3968-4379-a86f-9ceabde5faac", - "nextNodeCode": "dd515cdd-59f6-446f-94ca-25ca062afb42", - "coordinate": "220,200;310,200" + "nextNodeCode": "dd515cdd-59f6-446f-94ca-25ca062afb42" } - ] + ], + "nodeRatio": "0", + "coordinate": "200,200|200,200", + "formCustom": "N" }, { - "nodeType": "1", + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file,copy\"}]", "nodeCode": "dd515cdd-59f6-446f-94ca-25ca062afb42", "nodeName": "申请人", - "permissionFlag": "${initiator}", - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file,copy\"}]", - "coordinate": "360,200|360,200", - "version": "1", + "nodeType": 1, "skipList": [ { "skipType": "PASS", - "skipCondition": null, - "skipName": null, + "coordinate": "410,200;490,200", "nowNodeCode": "dd515cdd-59f6-446f-94ca-25ca062afb42", - "nextNodeCode": "78fa8e5b-e809-44ed-978a-41092409ebcf", - "coordinate": "410,200;490,200" + "nextNodeCode": "78fa8e5b-e809-44ed-978a-41092409ebcf" } - ] + ], + "nodeRatio": "0", + "coordinate": "360,200|360,200", + "formCustom": "N", + "permissionFlag": "warmFlowInitiator" }, { - "nodeType": "1", + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,copy,transfer,trust,file\"}]", "nodeCode": "78fa8e5b-e809-44ed-978a-41092409ebcf", "nodeName": "组长", - "permissionFlag": "role:1761300000000000001", - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,copy,transfer,trust,file\"}]", - "coordinate": "540,200|540,200", - "version": "1", + "nodeType": 1, "skipList": [ { "skipType": "PASS", - "skipCondition": null, - "skipName": null, + "coordinate": "590,200;670,200", "nowNodeCode": "78fa8e5b-e809-44ed-978a-41092409ebcf", - "nextNodeCode": "a8abf15f-b83e-428a-86cc-033555ea9bbe", - "coordinate": "590,200;670,200" + "nextNodeCode": "a8abf15f-b83e-428a-86cc-033555ea9bbe" } - ] + ], + "nodeRatio": "0", + "coordinate": "540,200|540,200", + "formCustom": "N", + "permissionFlag": "role:1761300000000000001" }, { - "nodeType": "1", + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,copy,transfer,trust,file\"}]", "nodeCode": "a8abf15f-b83e-428a-86cc-033555ea9bbe", "nodeName": "部门主管", - "permissionFlag": "role:1761300000000000003@@role:1761300000000000004", - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,copy,transfer,trust,file\"}]", - "coordinate": "720,200|720,200", - "version": "1", + "nodeType": 1, "skipList": [ { "skipType": "PASS", - "skipCondition": null, - "skipName": null, + "coordinate": "770,200;880,200", "nowNodeCode": "a8abf15f-b83e-428a-86cc-033555ea9bbe", - "nextNodeCode": "8b82b7d7-8660-455e-b880-d6d22ea3eb6d", - "coordinate": "770,200;880,200" + "nextNodeCode": "8b82b7d7-8660-455e-b880-d6d22ea3eb6d" } - ] + ], + "nodeRatio": "0", + "coordinate": "720,200|720,200", + "formCustom": "N", + "permissionFlag": "role:1761300000000000003@@role:1761300000000000004" }, { - "nodeType": "2", + "ext": "[]", "nodeCode": "8b82b7d7-8660-455e-b880-d6d22ea3eb6d", "nodeName": "结束", - "permissionFlag": null, - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": null, - "listenerPath": null, - "formCustom": "N", - "formPath": null, - "ext": "[]", + "nodeType": 2, + "nodeRatio": "0", "coordinate": "900,200|900,200", - "version": "1", - "skipList": [] + "formCustom": "N" } ], - "flowCode": "leave1", - "flowName": "请假申请-普通", - "modelValue": "CLASSICS", - "category": "1762300000000000103", - "version": "1", "formCustom": "N", - "formPath": "/workflow/leaveEdit/index", - "listenerType": null, - "listenerPath": null + "modelValue": "CLASSICS" } diff --git a/script/demo/demo_service_task.json b/script/demo/demo_service_task.json new file mode 100644 index 000000000..6e281e6ea --- /dev/null +++ b/script/demo/demo_service_task.json @@ -0,0 +1,281 @@ +{ + "version": "1", + "category": "1762300000000000100", + "flowCode": "demo_service_task", + "flowName": "服务任务示例", + "formPath": "/workflow/leaveEdit/index", + "nodeList": [ + { + "ext": "[]", + "nodeCode": "start", + "nodeName": "开始", + "nodeType": 0, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "80,260;190,260", + "nowNodeCode": "start", + "nextNodeCode": "submit" + } + ], + "nodeRatio": "0", + "coordinate": "60,260|60,260", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "approve", + "nodeName": "汇聚后审批", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "1730,260;1840,260", + "nowNodeCode": "approve", + "nextNodeCode": "end" + } + ], + "nodeRatio": "0", + "coordinate": "1710,260|1710,260", + "formCustom": "N", + "permissionFlag": "1761100000000000003" + }, + { + "ext": "[]", + "nodeCode": "bigDay", + "nodeName": "大于三天", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "1130,100;1240,260", + "nowNodeCode": "bigDay", + "nextNodeCode": "forkP" + } + ], + "nodeRatio": "0", + "coordinate": "1110,100|1110,100", + "formCustom": "N", + "permissionFlag": "1761100000000000003" + }, + { + "ext": "[]", + "nodeCode": "humanBranch", + "nodeName": "并行分支人工", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "1430,420;1540,260", + "nowNodeCode": "humanBranch", + "nextNodeCode": "joinP" + } + ], + "nodeRatio": "0", + "coordinate": "1410,420|1410,420", + "formCustom": "N", + "permissionFlag": "1761100000000000004" + }, + { + "ext": "[]", + "nodeCode": "smallDay", + "nodeName": "小于等于三天", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "1130,420;1240,260", + "nowNodeCode": "smallDay", + "nextNodeCode": "forkP" + } + ], + "nodeRatio": "0", + "coordinate": "1110,420|1110,420", + "formCustom": "N", + "permissionFlag": "1761100000000000004" + }, + { + "ext": "[]", + "nodeCode": "end", + "nodeName": "结束", + "nodeType": 2, + "nodeRatio": "0", + "coordinate": "1860,260|1860,260", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "gwDay", + "nodeName": "天数分支", + "nodeType": 3, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "980,260;1090,100", + "nowNodeCode": "gwDay", + "nextNodeCode": "bigDay", + "skipCondition": "gt@@day|3" + }, + { + "skipType": "PASS", + "coordinate": "980,260;1090,420", + "nowNodeCode": "gwDay", + "nextNodeCode": "smallDay" + } + ], + "nodeRatio": "0", + "coordinate": "960,260|960,260", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "forkP", + "nodeName": "并行网关", + "nodeType": 4, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "1280,260;1390,100", + "nowNodeCode": "forkP", + "nextNodeCode": "svcBranch" + }, + { + "skipType": "PASS", + "coordinate": "1280,260;1390,420", + "nowNodeCode": "forkP", + "nextNodeCode": "humanBranch" + } + ], + "nodeRatio": "0", + "coordinate": "1260,260|1260,260", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "joinP", + "nodeName": "汇聚网关", + "nodeType": 4, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "1580,260;1690,260", + "nowNodeCode": "joinP", + "nextNodeCode": "approve" + } + ], + "nodeRatio": "0", + "coordinate": "1560,260|1560,260", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "svcBranch", + "nodeName": "并行分支服务任务", + "nodeType": 6, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "1430,100;1540,260", + "nowNodeCode": "svcBranch", + "nextNodeCode": "joinP" + } + ], + "nodeRatio": "0", + "coordinate": "1410,100|1410,100", + "formCustom": "N", + "nodeConfig": "{\"serviceTask\":{\"type\":\"class\",\"handler\":\"org.dromara.workflow.demo.DemoClassServiceTask\"}}" + }, + { + "ext": "[]", + "nodeCode": "svcCalc", + "nodeName": "计算天数服务任务", + "nodeType": 6, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "830,260;940,260", + "nowNodeCode": "svcCalc", + "nextNodeCode": "gwDay" + } + ], + "nodeRatio": "0", + "coordinate": "810,260|810,260", + "formCustom": "N", + "nodeConfig": "{\"serviceTask\":{\"type\":\"expression\",\"expression\":\"#{@demoOrderService.evalDay(#execution)}\",\"resultVariable\":\"day\"}}" + }, + { + "ext": "[]", + "nodeCode": "svcClass", + "nodeName": "Java 类服务任务", + "nodeType": 6, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "380,260;490,260", + "nowNodeCode": "svcClass", + "nextNodeCode": "svcDelegate" + } + ], + "nodeRatio": "0", + "coordinate": "360,260|360,260", + "formCustom": "N", + "nodeConfig": "{\"serviceTask\":{\"type\":\"class\",\"handler\":\"org.dromara.workflow.demo.DemoClassServiceTask\"}}" + }, + { + "ext": "[]", + "nodeCode": "svcDelegate", + "nodeName": "Bean 委托表达式服务任务", + "nodeType": 6, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "530,260;640,260", + "nowNodeCode": "svcDelegate", + "nextNodeCode": "svcExpression" + } + ], + "nodeRatio": "0", + "coordinate": "510,260|510,260", + "formCustom": "N", + "nodeConfig": "{\"serviceTask\":{\"type\":\"delegateExpression\",\"expression\":\"#{@demoDelegateServiceTask}\"}}" + }, + { + "ext": "[]", + "nodeCode": "svcExpression", + "nodeName": "方法表达式服务任务", + "nodeType": 6, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "680,260;790,260", + "nowNodeCode": "svcExpression", + "nextNodeCode": "svcCalc" + } + ], + "nodeRatio": "0", + "coordinate": "660,260|660,260", + "formCustom": "N", + "nodeConfig": "{\"serviceTask\":{\"type\":\"expression\",\"expression\":\"#{@demoOrderService.createOrder(#execution)}\",\"resultVariable\":\"orderNo\"}}" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "submit", + "nodeName": "申请人", + "nodeType": 1, + "nodeRatio": "0", + "coordinate": "210,260|210,260", + "formCustom": "N", + "permissionFlag": "warmFlowInitiator", + "skipList": [ + { + "skipType": "PASS", + "coordinate": "230,260;340,260", + "nowNodeCode": "submit", + "nextNodeCode": "svcClass" + } + ] + } + ], + "formCustom": "N", + "modelValue": "CLASSICS" +} \ No newline at end of file diff --git a/script/demo/demo_timer_start.json b/script/demo/demo_timer_start.json new file mode 100644 index 000000000..0d2a9f692 --- /dev/null +++ b/script/demo/demo_timer_start.json @@ -0,0 +1,194 @@ +{ + "version": "1", + "category": "1762300000000000100", + "flowCode": "demo_timer_start", + "flowName": "定时启动示例(发布 30 秒后自动发起·带默认变量)", + "formPath": "/workflow/leaveEdit/index", + "nodeList": [ + { + "ext": "[]", + "nodeCode": "start", + "nodeName": "开始", + "nodeType": 0, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "88,260;190,260", + "nowNodeCode": "start", + "nextNodeCode": "timerStart" + } + ], + "nodeRatio": "0", + "coordinate": "60,260|60,260", + "formCustom": "N" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "inspect", + "nodeName": "执行数据巡检", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "470,260;555,260", + "nowNodeCode": "inspect", + "nextNodeCode": "gw1" + } + ], + "nodeRatio": "0", + "coordinate": "420,260|420,260", + "formCustom": "N", + "permissionFlag": "1761100000000000001" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "qa", + "nodeName": "质检复核", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "830,120;1020,120;1020,235", + "nowNodeCode": "qa", + "nextNodeCode": "gw2" + } + ], + "nodeRatio": "0", + "coordinate": "780,120|780,120", + "formCustom": "N", + "permissionFlag": "1761100000000000003" + }, + { + "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"copy,back,termination,file\"}]", + "nodeCode": "risk", + "nodeName": "风控否决判定", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "830,400;870,400", + "nowNodeCode": "risk", + "nextNodeCode": "gwRisk" + } + ], + "nodeRatio": "0", + "coordinate": "780,400|780,400", + "formCustom": "N", + "permissionFlag": "1761100000000000004" + }, + { + "ext": "[]", + "nodeCode": "terminate", + "nodeName": "严重异常终止", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "1070,400;1220,400;1220,288", + "nowNodeCode": "terminate", + "nextNodeCode": "end" + } + ], + "eventType": "TERMINATE_END", + "nodeRatio": "0", + "coordinate": "1020,400|1020,400", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "timerStart", + "nodeName": "发布 30 秒后自动发起", + "nodeType": 1, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "290,260;370,260", + "nowNodeCode": "timerStart", + "nextNodeCode": "inspect" + } + ], + "eventType": "TIMER_START", + "nodeRatio": "0", + "coordinate": "240,260|240,260", + "formCustom": "N", + "nodeConfig": "{\"variables\": {\"veto\": 0, \"channel\": \"daily\"}, \"event\": {\"timerType\": \"AFTER\", \"timerValue\": \"PT30S\", \"timezone\": \"Asia/Shanghai\"}}" + }, + { + "ext": "[]", + "nodeCode": "end", + "nodeName": "结束", + "nodeType": 2, + "nodeRatio": "0", + "coordinate": "1220,260|1220,260", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "gwRisk", + "nodeName": "是否严重异常", + "nodeType": 3, + "skipList": [ + { + "skipName": "严重异常", + "skipType": "PASS", + "coordinate": "925,400;970,400", + "nowNodeCode": "gwRisk", + "nextNodeCode": "terminate", + "skipCondition": "eq@@veto|1" + }, + { + "skipName": "无异常", + "skipType": "PASS", + "coordinate": "900,378;900,260;995,260", + "nowNodeCode": "gwRisk", + "nextNodeCode": "gw2" + } + ], + "nodeRatio": "0", + "coordinate": "900,400", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "gw1", + "nodeName": "并行分支", + "nodeType": 4, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "605,260;700,260;700,120;730,120", + "nowNodeCode": "gw1", + "nextNodeCode": "qa" + }, + { + "skipType": "PASS", + "coordinate": "605,260;700,260;700,400;730,400", + "nowNodeCode": "gw1", + "nextNodeCode": "risk" + } + ], + "nodeRatio": "0", + "coordinate": "580,260", + "formCustom": "N" + }, + { + "ext": "[]", + "nodeCode": "gw2", + "nodeName": "汇聚", + "nodeType": 4, + "skipList": [ + { + "skipType": "PASS", + "coordinate": "1045,260;1192,260", + "nowNodeCode": "gw2", + "nextNodeCode": "end" + } + ], + "nodeRatio": "0", + "coordinate": "1020,260", + "formCustom": "N" + } + ], + "formCustom": "N", + "modelValue": "CLASSICS" +} diff --git a/script/docker/docker-compose.yml b/script/docker/docker-compose.yml index 9a293c389..2619dee21 100644 --- a/script/docker/docker-compose.yml +++ b/script/docker/docker-compose.yml @@ -97,7 +97,7 @@ services: network_mode: "host" ruoyi-server1: - image: ruoyi/ruoyi-server:6.0.0 + image: ruoyi/ruoyi-server:6.1.0 container_name: ruoyi-server1 environment: # 时区上海 @@ -114,7 +114,7 @@ services: network_mode: "host" ruoyi-server2: - image: ruoyi/ruoyi-server:6.0.0 + image: ruoyi/ruoyi-server:6.1.0 container_name: ruoyi-server2 environment: # 时区上海 @@ -131,7 +131,7 @@ services: network_mode: "host" ruoyi-monitor-admin: - image: ruoyi/ruoyi-monitor-admin:6.0.0 + image: ruoyi/ruoyi-monitor-admin:6.1.0 container_name: ruoyi-monitor-admin environment: # 时区上海 @@ -143,7 +143,7 @@ services: network_mode: "host" ruoyi-snailjob-server: - image: ruoyi/ruoyi-snailjob-server:6.0.0 + image: ruoyi/ruoyi-snailjob-server:6.1.0 container_name: ruoyi-snailjob-server environment: # 时区上海 @@ -157,7 +157,7 @@ services: network_mode: "host" ruoyi-snailai-server: - image: ruoyi/ruoyi-snailai-server:6.0.0 + image: ruoyi/ruoyi-snailai-server:6.1.0 container_name: ruoyi-snailai-server environment: # 时区上海 diff --git a/script/leave/leave2.json b/script/leave/leave2.json deleted file mode 100644 index 262697cb8..000000000 --- a/script/leave/leave2.json +++ /dev/null @@ -1,187 +0,0 @@ -{ - "nodeList": [ - { - "nodeType": "0", - "nodeCode": "cef3895c-f7d8-4598-8bf3-8ec2ef6ce84a", - "nodeName": "开始", - "permissionFlag": null, - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": null, - "listenerPath": null, - "formCustom": "N", - "formPath": null, - "ext": "[]", - "coordinate": "300,240|300,240", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "cef3895c-f7d8-4598-8bf3-8ec2ef6ce84a", - "nextNodeCode": "fdcae93b-b69c-498a-b231-09255e74bcbd", - "coordinate": "320,240;390,240" - } - ] - }, - { - "nodeType": "1", - "nodeCode": "fdcae93b-b69c-498a-b231-09255e74bcbd", - "nodeName": "申请人", - "permissionFlag": "${initiator}", - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file\"}]", - "coordinate": "440,240|440,240", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "fdcae93b-b69c-498a-b231-09255e74bcbd", - "nextNodeCode": "7b8c7ead-7dc8-4951-a7f3-f0c41995909e", - "coordinate": "490,240;535,240" - } - ] - }, - { - "nodeType": "3", - "nodeCode": "7b8c7ead-7dc8-4951-a7f3-f0c41995909e", - "permissionFlag": null, - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": null, - "listenerPath": null, - "formCustom": "N", - "formPath": null, - "ext": "[]", - "coordinate": "560,240", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": "le@@leaveDays|2", - "skipName": null, - "nowNodeCode": "7b8c7ead-7dc8-4951-a7f3-f0c41995909e", - "nextNodeCode": "b3528155-dcb7-4445-bbdf-3d00e3499e86", - "coordinate": "560,265;560,320;670,320" - }, - { - "skipType": "PASS", - "skipCondition": "gt@@leaveDays|2", - "skipName": "大于两天", - "nowNodeCode": "7b8c7ead-7dc8-4951-a7f3-f0c41995909e", - "nextNodeCode": "5ed2362b-fc0c-4d52-831f-95208b830605", - "coordinate": "560,215;560,160;670,160|560,187" - } - ] - }, - { - "nodeType": "1", - "nodeCode": "b3528155-dcb7-4445-bbdf-3d00e3499e86", - "nodeName": "组长", - "permissionFlag": "1761100000000000003@@1761100000000000004", - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file,transfer,trust,copy\"}]", - "coordinate": "720,320|720,320", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "b3528155-dcb7-4445-bbdf-3d00e3499e86", - "nextNodeCode": "c9fa6d7d-2a74-4e78-b947-0cad8a6af869", - "coordinate": "770,320;860,320;860,280" - } - ] - }, - { - "nodeType": "1", - "nodeCode": "c9fa6d7d-2a74-4e78-b947-0cad8a6af869", - "nodeName": "总经理", - "permissionFlag": "role:1761300000000000001", - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file,transfer,trust,copy\"}]", - "coordinate": "860,240|860,240", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "c9fa6d7d-2a74-4e78-b947-0cad8a6af869", - "nextNodeCode": "40aa65fd-0712-4d23-b6f7-d0432b920fd1", - "coordinate": "910,240;980,240" - } - ] - }, - { - "nodeType": "2", - "nodeCode": "40aa65fd-0712-4d23-b6f7-d0432b920fd1", - "nodeName": "结束", - "permissionFlag": null, - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": null, - "listenerPath": null, - "formCustom": "N", - "formPath": null, - "ext": "[]", - "coordinate": "1000,240|1000,240", - "version": "1", - "skipList": [] - }, - { - "nodeType": "1", - "nodeCode": "5ed2362b-fc0c-4d52-831f-95208b830605", - "nodeName": "部门领导", - "permissionFlag": "role:1761300000000000001", - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file,transfer,trust,copy\"}]", - "coordinate": "720,160|720,160", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "5ed2362b-fc0c-4d52-831f-95208b830605", - "nextNodeCode": "c9fa6d7d-2a74-4e78-b947-0cad8a6af869", - "nextNodeType": "1", - "coordinate": "770,160;860,160;860,200" - } - ] - } - ], - "flowCode": "leave2", - "flowName": "请假申请-排他网关", - "modelValue": "CLASSICS", - "category": "1762300000000000103", - "version": "1", - "formCustom": "N", - "formPath": "/workflow/leaveEdit/index", - "listenerType": null, - "listenerPath": null -} diff --git a/script/leave/leave3.json b/script/leave/leave3.json deleted file mode 100644 index 1ac01e2bb..000000000 --- a/script/leave/leave3.json +++ /dev/null @@ -1,211 +0,0 @@ -{ - "nodeList": [ - { - "nodeType": "0", - "nodeCode": "a80ecf9f-f465-4ae5-a429-e30ec5d0f957", - "nodeName": "开始", - "permissionFlag": null, - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": null, - "listenerPath": null, - "formCustom": "N", - "formPath": null, - "ext": "[]", - "coordinate": "380,220|380,220", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "a80ecf9f-f465-4ae5-a429-e30ec5d0f957", - "nextNodeCode": "b7bbb571-06de-455c-8083-f83c07bf0b99", - "coordinate": "400,220;470,220" - } - ] - }, - { - "nodeType": "1", - "nodeCode": "b7bbb571-06de-455c-8083-f83c07bf0b99", - "nodeName": "申请人", - "permissionFlag": "${initiator}", - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file\"}]", - "coordinate": "520,220|520,220", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "b7bbb571-06de-455c-8083-f83c07bf0b99", - "nextNodeCode": "84d7ed24-bb44-4ba1-bf1f-e6f5092d3f0a", - "coordinate": "570,220;655,220" - } - ] - }, - { - "nodeType": "4", - "nodeCode": "84d7ed24-bb44-4ba1-bf1f-e6f5092d3f0a", - "permissionFlag": null, - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": null, - "listenerPath": null, - "formCustom": "N", - "formPath": null, - "ext": "[]", - "coordinate": "680,220", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "84d7ed24-bb44-4ba1-bf1f-e6f5092d3f0a", - "nextNodeCode": "4b7743cd-940c-431b-926f-e7b614fbf1fe", - "coordinate": "680,195;680,140;750,140" - }, - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "84d7ed24-bb44-4ba1-bf1f-e6f5092d3f0a", - "nextNodeCode": "762cb975-37d8-4276-b6db-79a4c3606394", - "coordinate": "680,245;680,300;750,300" - } - ] - }, - { - "nodeType": "1", - "nodeCode": "4b7743cd-940c-431b-926f-e7b614fbf1fe", - "nodeName": "市场部", - "permissionFlag": "role:1761300000000000001", - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file,transfer,trust,copy\"}]", - "coordinate": "800,140|800,140", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "4b7743cd-940c-431b-926f-e7b614fbf1fe", - "nextNodeCode": "b66b6563-f9fe-41cc-a782-f7837bb6f3d2", - "coordinate": "850,140;920,140;920,195" - } - ] - }, - { - "nodeType": "4", - "nodeCode": "b66b6563-f9fe-41cc-a782-f7837bb6f3d2", - "permissionFlag": null, - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": null, - "listenerPath": null, - "formCustom": "N", - "formPath": null, - "ext": "[]", - "coordinate": "920,220", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "b66b6563-f9fe-41cc-a782-f7837bb6f3d2", - "nextNodeCode": "23e7429e-2b47-4431-b93e-40db7c431ce6", - "coordinate": "945,220;975,220;975,220;960,220;960,220;990,220" - } - ] - }, - { - "nodeType": "1", - "nodeCode": "23e7429e-2b47-4431-b93e-40db7c431ce6", - "nodeName": "CEO", - "permissionFlag": "1761100000000000001", - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file,transfer,trust,copy\"}]", - "coordinate": "1040,220|1040,220", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "23e7429e-2b47-4431-b93e-40db7c431ce6", - "nextNodeCode": "f5ace37f-5a5e-4e64-a6f6-913ab9a71cd1", - "coordinate": "1090,220;1140,220" - } - ] - }, - { - "nodeType": "2", - "nodeCode": "f5ace37f-5a5e-4e64-a6f6-913ab9a71cd1", - "nodeName": "结束", - "permissionFlag": null, - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": null, - "listenerPath": null, - "formCustom": "N", - "formPath": null, - "ext": "[]", - "coordinate": "1160,220|1160,220", - "version": "1", - "skipList": [] - }, - { - "nodeType": "1", - "nodeCode": "762cb975-37d8-4276-b6db-79a4c3606394", - "nodeName": "综合部", - "permissionFlag": "role:1761300000000000003@@role:1761300000000000004", - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file,transfer,trust,copy\"}]", - "coordinate": "800,300|800,300", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "762cb975-37d8-4276-b6db-79a4c3606394", - "nextNodeCode": "b66b6563-f9fe-41cc-a782-f7837bb6f3d2", - "nextNodeType": "4", - "coordinate": "850,300;920,300;920,245" - } - ] - } - ], - "flowCode": "leave3", - "flowName": "请假申请-并行网关", - "modelValue": "CLASSICS", - "category": "1762300000000000103", - "version": "1", - "formCustom": "N", - "formPath": "/workflow/leaveEdit/index", - "listenerType": null, - "listenerPath": null -} diff --git a/script/leave/leave4.json b/script/leave/leave4.json deleted file mode 100644 index 7e2f57820..000000000 --- a/script/leave/leave4.json +++ /dev/null @@ -1,154 +0,0 @@ -{ - "nodeList": [ - { - "nodeType": "0", - "nodeCode": "9ce8bf00-f25b-4fc6-91b8-827082fc4876", - "nodeName": "开始", - "permissionFlag": null, - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": null, - "listenerPath": null, - "formCustom": "N", - "formPath": null, - "ext": "[]", - "coordinate": "320,240|320,240", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "9ce8bf00-f25b-4fc6-91b8-827082fc4876", - "nextNodeCode": "e90b98ef-35b4-410c-a663-bae8b7624b9f", - "coordinate": "340,240;410,240" - } - ] - }, - { - "nodeType": "1", - "nodeCode": "e90b98ef-35b4-410c-a663-bae8b7624b9f", - "nodeName": "申请人", - "permissionFlag": "${initiator}", - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file\"}]", - "coordinate": "460,240|460,240", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "e90b98ef-35b4-410c-a663-bae8b7624b9f", - "nextNodeCode": "768b5b1a-6726-4d67-8853-4cc70d5b1045", - "coordinate": "510,240;590,240" - } - ] - }, - { - "nodeType": "1", - "nodeCode": "768b5b1a-6726-4d67-8853-4cc70d5b1045", - "nodeName": "百分之60通过", - "permissionFlag": "${userList}", - "nodeRatio": "60.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file,addSign,subSign\"}]", - "coordinate": "640,240|640,240", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "768b5b1a-6726-4d67-8853-4cc70d5b1045", - "nextNodeCode": "2f9f2e21-9bcf-42a3-a07c-13037aad22d1", - "coordinate": "690,240;770,240" - } - ] - }, - { - "nodeType": "1", - "nodeCode": "2f9f2e21-9bcf-42a3-a07c-13037aad22d1", - "nodeName": "全部审批通过", - "permissionFlag": "role:1761300000000000001@@role:1761300000000000003", - "nodeRatio": "100.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file,addSign,subSign\"}]", - "coordinate": "820,240|820,240", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "2f9f2e21-9bcf-42a3-a07c-13037aad22d1", - "nextNodeCode": "27461e01-3d9f-4530-8fe3-bd5ec7f9571f", - "coordinate": "870,240;950,240" - } - ] - }, - { - "nodeType": "1", - "nodeCode": "27461e01-3d9f-4530-8fe3-bd5ec7f9571f", - "nodeName": "CEO", - "permissionFlag": "1761100000000000001", - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file,transfer,trust,copy\"}]", - "coordinate": "1000,240|1000,240", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "27461e01-3d9f-4530-8fe3-bd5ec7f9571f", - "nextNodeCode": "b62b88c3-8d8d-4969-911e-2aaea219e7fc", - "coordinate": "1050,240;1080,240;1080,240;1070,240;1070,240;1100,240" - } - ] - }, - { - "nodeType": "2", - "nodeCode": "b62b88c3-8d8d-4969-911e-2aaea219e7fc", - "nodeName": "结束", - "permissionFlag": null, - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": null, - "listenerPath": null, - "formCustom": "N", - "formPath": null, - "ext": "[]", - "coordinate": "1120,240|1120,240", - "version": "1", - "skipList": [] - } - ], - "flowCode": "leave4", - "flowName": "请假申请-会签", - "modelValue": "CLASSICS", - "category": "1762300000000000103", - "version": "1", - "formCustom": "N", - "formPath": "/workflow/leaveEdit/index", - "listenerType": null, - "listenerPath": null -} diff --git a/script/leave/leave5.json b/script/leave/leave5.json deleted file mode 100644 index 2cd24a00d..000000000 --- a/script/leave/leave5.json +++ /dev/null @@ -1,211 +0,0 @@ -{ - "nodeList": [ - { - "nodeType": "0", - "nodeCode": "ebebaf26-9cb6-497e-8119-4c9fed4c597c", - "nodeName": "开始", - "permissionFlag": null, - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": null, - "listenerPath": null, - "formCustom": "N", - "formPath": null, - "ext": "[]", - "coordinate": "300,220|300,220", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "ebebaf26-9cb6-497e-8119-4c9fed4c597c", - "nextNodeCode": "e1b04e96-dc81-4858-a309-2fe945d2f374", - "coordinate": "320,220;350,220;350,220;340,220;340,220;370,220" - } - ] - }, - { - "nodeType": "1", - "nodeCode": "e1b04e96-dc81-4858-a309-2fe945d2f374", - "nodeName": "申请人", - "permissionFlag": "${initiator}", - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file\"}]", - "coordinate": "420,220|420,220", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "e1b04e96-dc81-4858-a309-2fe945d2f374", - "nextNodeCode": "3e743f4f-51ca-41d4-8e94-21f5dd9b59c9", - "coordinate": "470,220;535,220" - } - ] - }, - { - "nodeType": "4", - "nodeCode": "3e743f4f-51ca-41d4-8e94-21f5dd9b59c9", - "permissionFlag": null, - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": null, - "listenerPath": null, - "formCustom": "N", - "formPath": null, - "ext": "[]", - "coordinate": "560,220", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "3e743f4f-51ca-41d4-8e94-21f5dd9b59c9", - "nextNodeCode": "c80f273e-1f17-4bd8-9ad1-04a4a94ea862", - "coordinate": "560,245;560,320;650,320" - }, - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "3e743f4f-51ca-41d4-8e94-21f5dd9b59c9", - "nextNodeCode": "1e3e8d3b-18ae-4d6c-a814-ce0d724adfa4", - "coordinate": "560,195;560,120;650,120" - } - ] - }, - { - "nodeType": "1", - "nodeCode": "c80f273e-1f17-4bd8-9ad1-04a4a94ea862", - "nodeName": "会签", - "permissionFlag": "role:1761300000000000001@@role:1761300000000000003", - "nodeRatio": "100.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file,addSign,subSign\"}]", - "coordinate": "700,320|700,320", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "c80f273e-1f17-4bd8-9ad1-04a4a94ea862", - "nextNodeCode": "1a20169e-3d82-4926-a151-e2daad28de1b", - "coordinate": "750,320;860,320;860,245" - } - ] - }, - { - "nodeType": "4", - "nodeCode": "1a20169e-3d82-4926-a151-e2daad28de1b", - "permissionFlag": null, - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": null, - "listenerPath": null, - "formCustom": "N", - "formPath": null, - "ext": "[]", - "coordinate": "860,220", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "1a20169e-3d82-4926-a151-e2daad28de1b", - "nextNodeCode": "7a8f0473-e409-442e-a843-5c2b813d00e9", - "coordinate": "885,220;950,220" - } - ] - }, - { - "nodeType": "1", - "nodeCode": "7a8f0473-e409-442e-a843-5c2b813d00e9", - "nodeName": "CEO", - "permissionFlag": "1761100000000000001", - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file,transfer,trust,copy\"}]", - "coordinate": "1000,220|1000,220", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "7a8f0473-e409-442e-a843-5c2b813d00e9", - "nextNodeCode": "03c4d2bc-58b5-4408-a2e4-65afb046f169", - "coordinate": "1050,220;1120,220" - } - ] - }, - { - "nodeType": "2", - "nodeCode": "03c4d2bc-58b5-4408-a2e4-65afb046f169", - "nodeName": "结束", - "permissionFlag": null, - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": null, - "listenerPath": null, - "formCustom": "N", - "formPath": null, - "ext": "[]", - "coordinate": "1140,220|1140,220", - "version": "1", - "skipList": [] - }, - { - "nodeType": "1", - "nodeCode": "1e3e8d3b-18ae-4d6c-a814-ce0d724adfa4", - "nodeName": "百分之60票签", - "permissionFlag": "${userList}", - "nodeRatio": "60.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file,addSign,subSign\"}]", - "coordinate": "700,120|700,120", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "1e3e8d3b-18ae-4d6c-a814-ce0d724adfa4", - "nextNodeCode": "1a20169e-3d82-4926-a151-e2daad28de1b", - "nextNodeType": "4", - "coordinate": "750,120;860,120;860,195" - } - ] - } - ], - "flowCode": "leave5", - "flowName": "请假申请-并行会签网关", - "modelValue": "CLASSICS", - "category": "1762300000000000103", - "version": "1", - "formCustom": "N", - "formPath": "/workflow/leaveEdit/index", - "listenerType": null, - "listenerPath": null -} diff --git a/script/leave/leave6.json b/script/leave/leave6.json deleted file mode 100644 index 348eb5c54..000000000 --- a/script/leave/leave6.json +++ /dev/null @@ -1,368 +0,0 @@ -{ - "nodeList": [ - { - "nodeType": "0", - "nodeCode": "122b89a5-7c6f-40a3-aa09-7a263f902054", - "nodeName": "开始", - "permissionFlag": null, - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": null, - "listenerPath": null, - "formCustom": "N", - "formPath": null, - "ext": "[]", - "coordinate": "240,300|240,300", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "122b89a5-7c6f-40a3-aa09-7a263f902054", - "nextNodeCode": "c25a0e86-fdd1-4f03-8e22-14db70389dbd", - "coordinate": "260,300;350,300" - } - ] - }, - { - "nodeType": "1", - "nodeCode": "c25a0e86-fdd1-4f03-8e22-14db70389dbd", - "nodeName": "申请人", - "permissionFlag": "${initiator}", - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file\"}]", - "coordinate": "400,300|400,300", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "c25a0e86-fdd1-4f03-8e22-14db70389dbd", - "nextNodeCode": "07ecda1d-7a0a-47b5-8a91-6186c9473742", - "coordinate": "450,300;510,300" - } - ] - }, - { - "nodeType": "1", - "nodeCode": "2bfa3919-78cf-4bc1-b59b-df463a4546f9", - "nodeName": "副经理", - "permissionFlag": "role:1761300000000000001@@role:1761300000000000003@@role:1761300000000000004", - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination\"}]", - "coordinate": "860,200|860,200", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "2bfa3919-78cf-4bc1-b59b-df463a4546f9", - "nextNodeCode": "394e1cc8-b8b2-4189-9f81-44448e88ac32", - "coordinate": "910,200;1000,200;1000,275" - } - ] - }, - { - "nodeType": "1", - "nodeCode": "ec17f60e-94e0-4d96-a3ce-3417e9d32d60", - "nodeName": "组长", - "permissionFlag": "1761100000000000001", - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination\"}]", - "coordinate": "860,400|860,400", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "ec17f60e-94e0-4d96-a3ce-3417e9d32d60", - "nextNodeCode": "394e1cc8-b8b2-4189-9f81-44448e88ac32", - "coordinate": "910,400;1000,400;1000,325" - } - ] - }, - { - "nodeType": "1", - "nodeCode": "07ecda1d-7a0a-47b5-8a91-6186c9473742", - "nodeName": "副组长", - "permissionFlag": "1761100000000000001", - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,transfer,copy,pop\"}]", - "coordinate": "560,300|560,300", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "07ecda1d-7a0a-47b5-8a91-6186c9473742", - "nextNodeCode": "48117e2c-6328-406b-b102-c4a9d115bb13", - "coordinate": "610,300;675,300" - } - ] - }, - { - "nodeType": "3", - "nodeCode": "48117e2c-6328-406b-b102-c4a9d115bb13", - "permissionFlag": null, - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": null, - "listenerPath": null, - "formCustom": "N", - "formPath": null, - "ext": "[]", - "coordinate": "700,300", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": "default@@${leaveDays > 2}", - "skipName": "大于两天", - "nowNodeCode": "48117e2c-6328-406b-b102-c4a9d115bb13", - "nextNodeCode": "2bfa3919-78cf-4bc1-b59b-df463a4546f9", - "nextNodeType": "1", - "coordinate": "700,275;700,200;810,200|700,237" - }, - { - "skipType": "PASS", - "skipCondition": "spel@@#{@testLeaveServiceImpl.eval(#leaveDays)}", - "skipName": null, - "nowNodeCode": "48117e2c-6328-406b-b102-c4a9d115bb13", - "nextNodeCode": "ec17f60e-94e0-4d96-a3ce-3417e9d32d60", - "nextNodeType": "1", - "coordinate": "700,325;700,400;810,400" - } - ] - }, - { - "nodeType": "3", - "nodeCode": "394e1cc8-b8b2-4189-9f81-44448e88ac32", - "permissionFlag": null, - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": null, - "listenerPath": null, - "formCustom": "N", - "formPath": null, - "ext": "[]", - "coordinate": "1000,300", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "394e1cc8-b8b2-4189-9f81-44448e88ac32", - "nextNodeCode": "9c93a195-cff2-4e17-ab0a-a4f264191496", - "coordinate": "1025,300;1130,300" - } - ] - }, - { - "nodeType": "1", - "nodeCode": "9c93a195-cff2-4e17-ab0a-a4f264191496", - "nodeName": "经理会签", - "permissionFlag": "1761100000000000001@@1761100000000000003", - "nodeRatio": "100.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,pop,addSign,subSign\"}]", - "coordinate": "1180,300|1180,300", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "9c93a195-cff2-4e17-ab0a-a4f264191496", - "nextNodeCode": "a1a42056-afd1-4e90-88bc-36cbf5a66992", - "coordinate": "1230,300;1315,300" - } - ] - }, - { - "nodeType": "4", - "nodeCode": "a1a42056-afd1-4e90-88bc-36cbf5a66992", - "permissionFlag": null, - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": null, - "listenerPath": null, - "formCustom": "N", - "formPath": null, - "ext": "[]", - "coordinate": "1340,300", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "a1a42056-afd1-4e90-88bc-36cbf5a66992", - "nextNodeCode": "fcfdd9f6-f526-4c1a-b71d-88afa31aebc5", - "coordinate": "1340,325;1340,400;1430,400" - }, - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "a1a42056-afd1-4e90-88bc-36cbf5a66992", - "nextNodeCode": "350dfa0c-a77c-4efa-8527-10efa02d8be4", - "coordinate": "1340,275;1340,200;1430,200" - } - ] - }, - { - "nodeType": "1", - "nodeCode": "350dfa0c-a77c-4efa-8527-10efa02d8be4", - "nodeName": "总经理", - "permissionFlag": "1761100000000000001@@1761100000000000003", - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination\"}]", - "coordinate": "1480,200|1480,200", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "350dfa0c-a77c-4efa-8527-10efa02d8be4", - "nextNodeCode": "c36a46ef-04f9-463f-bad7-4b395c818519", - "coordinate": "1530,200;1640,200;1640,275" - } - ] - }, - { - "nodeType": "1", - "nodeCode": "fcfdd9f6-f526-4c1a-b71d-88afa31aebc5", - "nodeName": "副总经理", - "permissionFlag": "1761100000000000001@@1761100000000000003", - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination\"}]", - "coordinate": "1480,400|1480,400", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "fcfdd9f6-f526-4c1a-b71d-88afa31aebc5", - "nextNodeCode": "c36a46ef-04f9-463f-bad7-4b395c818519", - "coordinate": "1530,400;1640,400;1640,325" - } - ] - }, - { - "nodeType": "4", - "nodeCode": "c36a46ef-04f9-463f-bad7-4b395c818519", - "permissionFlag": null, - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": null, - "listenerPath": null, - "formCustom": "N", - "formPath": null, - "ext": "[]", - "coordinate": "1640,300", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "c36a46ef-04f9-463f-bad7-4b395c818519", - "nextNodeCode": "3fcea762-b53a-4ae1-8365-7bec90444828", - "coordinate": "1665,300;1770,300" - } - ] - }, - { - "nodeType": "1", - "nodeCode": "3fcea762-b53a-4ae1-8365-7bec90444828", - "nodeName": "董事", - "permissionFlag": "1761100000000000001", - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": "", - "listenerPath": "", - "formCustom": "N", - "formPath": null, - "ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination\"}]", - "coordinate": "1820,300|1820,300", - "version": "1", - "skipList": [ - { - "skipType": "PASS", - "skipCondition": null, - "skipName": null, - "nowNodeCode": "3fcea762-b53a-4ae1-8365-7bec90444828", - "nextNodeCode": "9cfbfd3e-6c04-41d6-9fc2-6787a7d2cd31", - "coordinate": "1870,300;1960,300" - } - ] - }, - { - "nodeType": "2", - "nodeCode": "9cfbfd3e-6c04-41d6-9fc2-6787a7d2cd31", - "nodeName": "结束", - "permissionFlag": null, - "nodeRatio": "0.000", - "anyNodeSkip": null, - "listenerType": null, - "listenerPath": null, - "formCustom": "N", - "formPath": null, - "ext": "[]", - "coordinate": "1980,300|1980,300", - "version": "1", - "skipList": [] - } - ], - "flowCode": "leave6", - "flowName": "请假申请-排他并行会签", - "modelValue": "CLASSICS", - "category": "1762300000000000103", - "version": "1", - "formCustom": "N", - "formPath": "/workflow/leaveEdit/index", - "listenerType": null, - "listenerPath": null -} diff --git a/script/sql/oracle/oracle_ry_workflow.sql b/script/sql/oracle/oracle_ry_workflow.sql index 11df353c2..c80227ae7 100644 --- a/script/sql/oracle/oracle_ry_workflow.sql +++ b/script/sql/oracle/oracle_ry_workflow.sql @@ -12,10 +12,11 @@ create table FLOW_DEFINITION IS_PUBLISH NUMBER(1) default 0 not null, FORM_CUSTOM VARCHAR2(1) default 'N', FORM_PATH VARCHAR2(100), - ACTIVITY_STATUS NUMBER(1) default 1, + ACTIVITY_STATUS NUMBER(1) default 1 not null, LISTENER_TYPE VARCHAR2(100), - LISTENER_PATH VARCHAR2(500), + LISTENER_PATH VARCHAR2(400), EXT VARCHAR2(500), + DEF_CONFIG CLOB, CREATE_TIME DATE, CREATE_BY VARCHAR2(64) default '', UPDATE_TIME DATE, @@ -41,6 +42,7 @@ comment on column FLOW_DEFINITION.ACTIVITY_STATUS is '流程激活状态(0挂 comment on column FLOW_DEFINITION.LISTENER_TYPE is '监听器类型'; comment on column FLOW_DEFINITION.LISTENER_PATH is '监听器路径'; comment on column FLOW_DEFINITION.EXT is '扩展字段,预留给业务系统使用'; +comment on column FLOW_DEFINITION.DEF_CONFIG is '流程定义配置(自动审批等引擎配置与自定义扩展属性json)'; comment on column FLOW_DEFINITION.CREATE_TIME is '创建时间'; comment on column FLOW_DEFINITION.CREATE_BY is '创建人'; comment on column FLOW_DEFINITION.UPDATE_TIME is '更新时间'; @@ -60,15 +62,19 @@ create table FLOW_NODE COORDINATE VARCHAR2(100), ANY_NODE_SKIP VARCHAR2(100), LISTENER_TYPE VARCHAR2(100), - LISTENER_PATH VARCHAR2(500), + LISTENER_PATH VARCHAR2(400), FORM_CUSTOM VARCHAR2(1) default 'N', FORM_PATH VARCHAR2(100), - VERSION VARCHAR2(20), CREATE_TIME DATE, CREATE_BY VARCHAR2(64) default '', UPDATE_TIME DATE, UPDATE_BY VARCHAR2(64) default '', EXT CLOB, + NODE_CONFIG CLOB, + EVENT_TYPE VARCHAR2(40), + EVENT_NAME VARCHAR2(100), + ATTACHED_TO_NODE_CODE VARCHAR2(100), + INTERRUPTING NUMBER(1), DEL_FLAG VARCHAR2(1) default '0', TENANT_ID VARCHAR2(40) ); @@ -89,12 +95,16 @@ comment on column FLOW_NODE.LISTENER_TYPE is '监听器类型'; comment on column FLOW_NODE.LISTENER_PATH is '监听器路径'; comment on column FLOW_NODE.FORM_CUSTOM is '审批表单是否自定义 (Y是 N否)'; comment on column FLOW_NODE.FORM_PATH is '审批表单路径'; -comment on column FLOW_NODE.VERSION is '版本'; comment on column FLOW_NODE.CREATE_TIME is '创建时间'; comment on column FLOW_NODE.CREATE_BY is '创建人'; comment on column FLOW_NODE.UPDATE_TIME is '更新时间'; comment on column FLOW_NODE.UPDATE_BY is '更新人'; comment on column FLOW_NODE.EXT is '节点扩展属性'; +comment on column FLOW_NODE.NODE_CONFIG is '节点配置(服务任务、事件等引擎节点配置json)'; +comment on column FLOW_NODE.EVENT_TYPE is '事件类型(MESSAGE_START TIMER_START TIMER_CATCH MESSAGE_CATCH MESSAGE_THROW TIMER_BOUNDARY MESSAGE_BOUNDARY ERROR_CATCH ERROR_BOUNDARY TERMINATE_END MESSAGE_END)'; +comment on column FLOW_NODE.EVENT_NAME is '事件名称(消息事件使用)'; +comment on column FLOW_NODE.ATTACHED_TO_NODE_CODE is '边界事件附着的任务节点编码'; +comment on column FLOW_NODE.INTERRUPTING is '边界事件是否中断型'; comment on column FLOW_NODE.DEL_FLAG is '删除标志'; comment on column FLOW_NODE.TENANT_ID is '租户id'; comment on column FLOW_NODE.PERMISSION_FLAG is '权限标识(权限类型:权限标识,可以多个,用@@隔开)'; @@ -145,12 +155,12 @@ create table FLOW_INSTANCE ID NUMBER not null, DEFINITION_ID NUMBER not null, BUSINESS_ID VARCHAR2(40) not null, - NODE_TYPE NUMBER(1), - NODE_CODE VARCHAR2(100), + NODE_TYPE NUMBER(1) not null, + NODE_CODE VARCHAR2(100) not null, NODE_NAME VARCHAR2(100), VARIABLE CLOB, - FLOW_STATUS VARCHAR2(20), - ACTIVITY_STATUS NUMBER(1) default 1, + FLOW_STATUS VARCHAR2(20) not null, + ACTIVITY_STATUS NUMBER(1) default 1 not null, DEF_JSON CLOB, CREATE_TIME DATE, CREATE_BY VARCHAR2(64) default '', @@ -172,7 +182,7 @@ comment on column FLOW_INSTANCE.NODE_TYPE is '开始节点类型 (0开始节点 comment on column FLOW_INSTANCE.NODE_CODE is '开始节点编码'; comment on column FLOW_INSTANCE.NODE_NAME is '开始节点名称'; comment on column FLOW_INSTANCE.VARIABLE is '任务变量'; -comment on column FLOW_INSTANCE.FLOW_STATUS is '流程状态(0待提交 1审批中 2审批通过 4终止 5作废 6撤销 8已完成 9已退回 10失效 11拿回)'; +comment on column FLOW_INSTANCE.FLOW_STATUS is '实例状态(draft待提交 waiting审批中 termination终止 invalid作废 cancel撤销 finish已完成 back已退回 restart重启)'; comment on column FLOW_INSTANCE.ACTIVITY_STATUS is '流程激活状态(0挂起 1激活)'; comment on column FLOW_INSTANCE.DEF_JSON is '流程定义json'; comment on column FLOW_INSTANCE.CREATE_TIME is '创建时间'; @@ -188,10 +198,10 @@ create table FLOW_TASK ID NUMBER(20) not null, DEFINITION_ID NUMBER(20) not null, INSTANCE_ID NUMBER(20) not null, - NODE_CODE VARCHAR2(100), + NODE_CODE VARCHAR2(100) not null, NODE_NAME VARCHAR2(100), - NODE_TYPE NUMBER(1), - FLOW_STATUS VARCHAR2(20), + NODE_TYPE NUMBER(1) not null, + FLOW_STATUS VARCHAR2(20) not null, FORM_CUSTOM VARCHAR2(1) default 'N', FORM_PATH VARCHAR2(100), CREATE_TIME DATE, @@ -212,7 +222,7 @@ comment on column FLOW_TASK.INSTANCE_ID is '对应flow_instance表的id'; comment on column FLOW_TASK.NODE_CODE is '节点编码'; comment on column FLOW_TASK.NODE_NAME is '节点名称'; comment on column FLOW_TASK.NODE_TYPE is '节点类型 (0开始节点 1中间节点 2结束节点 3互斥网关 4并行网关)'; -comment on column FLOW_TASK.FLOW_STATUS is '流程状态(0待提交 1审批中 2审批通过 4终止 5作废 6撤销 8已完成 9已退回 10失效 11拿回)'; +comment on column FLOW_TASK.FLOW_STATUS is '任务状态(pending待审批 pass通过 auto_pass自动审批 back退回 invalid失效 take_back拿回 copy抄送;任务结果,勿套用实例状态编码)'; comment on column FLOW_TASK.FORM_CUSTOM is '审批表单是否自定义 (Y是 N否)'; comment on column FLOW_TASK.FORM_PATH is '审批表单路径'; comment on column FLOW_TASK.CREATE_TIME is '创建时间'; @@ -234,15 +244,16 @@ create table FLOW_HIS_TASK TARGET_NODE_CODE VARCHAR2(200), TARGET_NODE_NAME VARCHAR2(200), APPROVER VARCHAR2(40), - COOPERATE_TYPE NUMBER(1) default 0, + COOPERATE_TYPE VARCHAR2(20) default 'none' not null, COLLABORATOR VARCHAR2(500), - SKIP_TYPE VARCHAR2(10), - FLOW_STATUS VARCHAR2(20), + SKIP_TYPE VARCHAR2(10) not null, + FLOW_STATUS VARCHAR2(20) not null, FORM_CUSTOM VARCHAR2(1) default 'N', FORM_PATH VARCHAR2(100), MESSAGE VARCHAR2(500), VARIABLE CLOB, EXT CLOB, + NODE_CONFIG CLOB, CREATE_TIME DATE, UPDATE_TIME DATE, DEL_FLAG VARCHAR2(1) default '0', @@ -264,18 +275,19 @@ comment on column FLOW_HIS_TASK.NODE_TYPE is '开始节点类型 (0开始节点 comment on column FLOW_HIS_TASK.TARGET_NODE_CODE is '目标节点编码'; comment on column FLOW_HIS_TASK.TARGET_NODE_NAME is '目标节点名称'; comment on column FLOW_HIS_TASK.SKIP_TYPE is '流转类型(PASS通过 REJECT退回 NONE无动作)'; -comment on column FLOW_HIS_TASK.FLOW_STATUS is '流程状态(0待提交 1审批中 2审批通过 4终止 5作废 6撤销 8已完成 9已退回 10失效 11拿回)'; +comment on column FLOW_HIS_TASK.FLOW_STATUS is '任务状态(pending待审批 pass通过 auto_pass自动审批 back退回 invalid失效 take_back拿回 copy抄送;任务结果,勿套用实例状态编码,唯一例外是撤回归档写 cancel)'; comment on column FLOW_HIS_TASK.FORM_CUSTOM is '审批表单是否自定义 (Y是 N否)'; comment on column FLOW_HIS_TASK.FORM_PATH is '审批表单路径'; comment on column FLOW_HIS_TASK.MESSAGE is '审批意见'; comment on column FLOW_HIS_TASK.VARIABLE is '任务变量'; comment on column FLOW_HIS_TASK.EXT is '扩展字段,预留给业务系统使用'; +comment on column FLOW_HIS_TASK.NODE_CONFIG is '节点配置(服务任务等引擎配置与自定义扩展属性json)'; comment on column FLOW_HIS_TASK.CREATE_TIME is '任务开始时间'; comment on column FLOW_HIS_TASK.UPDATE_TIME is '审批完成时间'; comment on column FLOW_HIS_TASK.DEL_FLAG is '删除标志'; comment on column FLOW_HIS_TASK.TENANT_ID is '租户id'; comment on column FLOW_HIS_TASK.APPROVER is '审批者'; -comment on column FLOW_HIS_TASK.COOPERATE_TYPE is '协作方式(1审批 2转办 3委派 4会签 5票签 6加签 7减签)'; +comment on column FLOW_HIS_TASK.COOPERATE_TYPE is '协作方式(none无 transfer转办 delegate委派 add_signer加签 remove_signer减签;行为维度,抄送/会签/票签不落此列,结果见 flow_status)'; comment on column FLOW_HIS_TASK.COLLABORATOR is '协作人'; create table FLOW_USER @@ -285,7 +297,7 @@ create table FLOW_USER PROCESSED_BY VARCHAR2(80), ASSOCIATED NUMBER(20) not null, CREATE_TIME DATE, - CREATE_BY VARCHAR2(64) default '', + CREATE_BY VARCHAR2(80), UPDATE_TIME DATE, UPDATE_BY VARCHAR2(64) default '', DEL_FLAG VARCHAR2(1) default '0', @@ -297,9 +309,9 @@ alter table FLOW_USER comment on table FLOW_USER is '待办任务表'; comment on column FLOW_USER.ID is '主键id'; -comment on column FLOW_USER.TYPE is '人员类型(1待办任务的审批人权限 2待办任务的转办人权限 3待办任务的委托人权限)'; +comment on column FLOW_USER.TYPE is '人员类型(1待办任务的审批人权限 2待办任务的转办人权限 3待办任务的委托人权限 4待办任务的抄送人)'; comment on column FLOW_USER.PROCESSED_BY is '权限人)'; -comment on column FLOW_USER.ASSOCIATED is '任务表id'; +comment on column FLOW_USER.ASSOCIATED is '关联id(待办任务id;抄送人记录为 flow_his_task.id)'; comment on column FLOW_USER.CREATE_TIME is '创建时间'; comment on column FLOW_USER.CREATE_BY is '创建人'; comment on column FLOW_USER.UPDATE_TIME is '更新时间'; @@ -310,6 +322,79 @@ comment on column FLOW_USER.TENANT_ID is '租户id'; create index USER_PROCESSED_TYPE on FLOW_USER (PROCESSED_BY, TYPE); create index USER_ASSOCIATED_IDX on FLOW_USER (ASSOCIATED); +-- ---------------------------- +-- 事件等待表 +-- ---------------------------- +-- 定时 / 消息等需要“等待”的事件统一落在此表,状态独立于流程状态。 +create table FLOW_EVENT_WAIT +( + ID NUMBER(20) not null, + TENANT_ID VARCHAR2(40), + DEL_FLAG VARCHAR2(1) default '0', + DEFINITION_ID NUMBER(20), + INSTANCE_ID NUMBER(20), + NODE_CODE VARCHAR2(100), + EVENT_TYPE VARCHAR2(40) not null, + EVENT_NAME VARCHAR2(100), + ATTACHED_TASK_ID NUMBER(20), + TRIGGER_TYPE VARCHAR2(40), + TRIGGER_INTERVAL VARCHAR2(100), + NEXT_TRIGGER_AT DATE, + TIMEZONE VARCHAR2(40), + REPEAT_COUNT NUMBER(10), + MAX_REPEAT_COUNT NUMBER(10), + STATUS VARCHAR2(20) not null, + RETRY_COUNT NUMBER(10), + MAX_RETRY_COUNT NUMBER(10), + RETRY_AT DATE, + LOCK_OWNER VARCHAR2(64), + LOCK_UNTIL DATE, + VERSION NUMBER(10), + PAYLOAD CLOB, + LAST_ERROR VARCHAR2(500), + CREATE_TIME DATE, + UPDATE_TIME DATE +); + +alter table FLOW_EVENT_WAIT + add constraint PK_FLOW_EVENT_WAIT primary key (ID); + +comment on table FLOW_EVENT_WAIT is '事件等待表'; +comment on column FLOW_EVENT_WAIT.ID is '主键id'; +comment on column FLOW_EVENT_WAIT.TENANT_ID is '租户id'; +comment on column FLOW_EVENT_WAIT.DEL_FLAG is '删除标志'; +comment on column FLOW_EVENT_WAIT.DEFINITION_ID is '流程定义id(定时启动事件必须有)'; +comment on column FLOW_EVENT_WAIT.INSTANCE_ID is '流程实例id(实例等待事件必须有)'; +comment on column FLOW_EVENT_WAIT.NODE_CODE is '事件节点编码(定位沿用 definition_id + node_code)'; +comment on column FLOW_EVENT_WAIT.EVENT_TYPE is '事件类型(MESSAGE_START TIMER_START TIMER_CATCH MESSAGE_CATCH MESSAGE_THROW TIMER_BOUNDARY MESSAGE_BOUNDARY ERROR_CATCH ERROR_BOUNDARY TERMINATE_END MESSAGE_END)'; +comment on column FLOW_EVENT_WAIT.EVENT_NAME is '事件名称(消息事件使用)'; +comment on column FLOW_EVENT_WAIT.ATTACHED_TASK_ID is '边界事件关联的待办任务id'; +comment on column FLOW_EVENT_WAIT.TRIGGER_TYPE is '触发策略类型(AFTER FIXED POINT_IN_TIME)'; +comment on column FLOW_EVENT_WAIT.TRIGGER_INTERVAL is '触发策略原始配置'; +comment on column FLOW_EVENT_WAIT.NEXT_TRIGGER_AT is '下次触发时间'; +comment on column FLOW_EVENT_WAIT.TIMEZONE is '定时触发使用的时区'; +comment on column FLOW_EVENT_WAIT.REPEAT_COUNT is '已触发次数'; +comment on column FLOW_EVENT_WAIT.MAX_REPEAT_COUNT is '最大触发次数'; +comment on column FLOW_EVENT_WAIT.STATUS is '等待状态(WAITING RUNNING CANCELLED RETRY DEAD)'; +comment on column FLOW_EVENT_WAIT.RETRY_COUNT is '已重试次数'; +comment on column FLOW_EVENT_WAIT.MAX_RETRY_COUNT is '最大重试次数'; +comment on column FLOW_EVENT_WAIT.RETRY_AT is '下次重试时间'; +comment on column FLOW_EVENT_WAIT.LOCK_OWNER is '抢占持有者标识'; +comment on column FLOW_EVENT_WAIT.LOCK_UNTIL is '抢占租约到期时间'; +comment on column FLOW_EVENT_WAIT.VERSION is '乐观锁版本号'; +comment on column FLOW_EVENT_WAIT.PAYLOAD is '消息事件接收的业务载荷'; +comment on column FLOW_EVENT_WAIT.LAST_ERROR is '最近一次失败原因摘要'; +comment on column FLOW_EVENT_WAIT.CREATE_TIME is '创建时间'; +comment on column FLOW_EVENT_WAIT.UPDATE_TIME is '更新时间'; + +create index EVENT_WAIT_STATUS_NEXT on FLOW_EVENT_WAIT (STATUS, NEXT_TRIGGER_AT, ID); +create index EVENT_WAIT_STATUS_RETRY on FLOW_EVENT_WAIT (STATUS, RETRY_AT, ID); +create index EVENT_WAIT_TENANT_NEXT on FLOW_EVENT_WAIT (TENANT_ID, STATUS, NEXT_TRIGGER_AT, ID); +create index EVENT_WAIT_TENANT_RETRY on FLOW_EVENT_WAIT (TENANT_ID, STATUS, RETRY_AT, ID); +create index EVENT_WAIT_INSTANCE_TASK on FLOW_EVENT_WAIT (INSTANCE_ID, ATTACHED_TASK_ID); +create index EVENT_WAIT_NAME_INST_STATUS on FLOW_EVENT_WAIT (EVENT_NAME, INSTANCE_ID, STATUS); +create index EVENT_WAIT_DEF_NODE_STATUS on FLOW_EVENT_WAIT (DEFINITION_ID, NODE_CODE, STATUS); + -- ---------------------------- -- 流程分类表 -- ---------------------------- @@ -516,6 +601,11 @@ INSERT INTO sys_menu VALUES (1761400000000011804, '流程spel表达式定义修 INSERT INTO sys_menu VALUES (1761400000000011805, '流程spel表达式定义删除', 1761400000000011801, 4, '#', '', NULL, 'N', 'Y', 'F', '0', '0', 'workflow:spel:remove', '#', '', '', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, ''); INSERT INTO sys_menu VALUES (1761400000000011806, '流程spel表达式定义导出', 1761400000000011801, 5, '#', '', NULL, 'N', 'Y', 'F', '0', '0', 'workflow:spel:export', '#', '', '', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, ''); +-- 定时任务(事件等待记录)菜单与按钮 +INSERT INTO sys_menu VALUES (1761400000000011901, '定时任务', 1761400000000011630, 3, 'timer', 'workflow/timer/index', '', 'N', 'N', 'C', '0', '0', 'workflow:eventWait:list', 'time', '', '', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '定时任务与事件等待监控菜单'); +INSERT INTO sys_menu VALUES (1761400000000011902, '定时任务查询', 1761400000000011901, 1, '#', '', NULL, 'N', 'Y', 'F', '0', '0', 'workflow:eventWait:query', '#', '', '', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, ''); +INSERT INTO sys_menu VALUES (1761400000000011903, '定时任务手动补发', 1761400000000011901, 2, '#', '', NULL, 'N', 'Y', 'F', '0', '0', 'workflow:eventWait:trigger', '#', '', '', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, ''); + INSERT INTO sys_menu VALUES (1761400000000011638, '请假申请', 1761400000000000005, 1, 'leave', 'workflow/leave/index', '', 'N', 'Y', 'C', '0', '0', 'workflow:leave:list', '#', '', '', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '请假申请菜单'); INSERT INTO sys_menu VALUES (1761400000000011639, '请假申请查询', 1761400000000011638, 1, '#', '', '', 'N', 'Y', 'F', '0', '0', 'workflow:leave:query', '#', '', '', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, ''); INSERT INTO sys_menu VALUES (1761400000000011640, '请假申请新增', 1761400000000011638, 2, '#', '', '', 'N', 'Y', 'F', '0', '0', 'workflow:leave:add', '#', '', '', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, ''); @@ -533,17 +623,18 @@ INSERT INTO sys_dict_data VALUES (1761600000000000042, 4, '已完成', 'finish', INSERT INTO sys_dict_data VALUES (1761600000000000043, 5, '已作废', 'invalid', 'wf_business_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '已作废'); INSERT INTO sys_dict_data VALUES (1761600000000000044, 6, '已退回', 'back', 'wf_business_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '已退回'); INSERT INTO sys_dict_data VALUES (1761600000000000045, 7, '已终止', 'termination', 'wf_business_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '已终止'); +INSERT INTO sys_dict_data VALUES (1761600000000000063, 8, '重启', 'restart', 'wf_business_status', '', 'info', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '重启'); INSERT INTO sys_dict_data VALUES (1761600000000000046, 1, '自定义表单', 'static', 'wf_form_type', '', 'success', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '自定义表单'); INSERT INTO sys_dict_data VALUES (1761600000000000047, 2, '动态表单', 'dynamic', 'wf_form_type', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '动态表单'); -INSERT INTO sys_dict_data VALUES (1761600000000000048, 1, '撤销', 'cancel', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '撤销'); +INSERT INTO sys_dict_data VALUES (1761600000000000060, 1, '待审批', 'pending', 'wf_task_status', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '待审批'); INSERT INTO sys_dict_data VALUES (1761600000000000049, 2, '通过', 'pass', 'wf_task_status', '', 'success', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '通过'); -INSERT INTO sys_dict_data VALUES (1761600000000000050, 3, '待审核', 'waiting', 'wf_task_status', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '待审核'); -INSERT INTO sys_dict_data VALUES (1761600000000000051, 4, '作废', 'invalid', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '作废'); +INSERT INTO sys_dict_data VALUES (1761600000000000061, 3, '自动审批', 'auto_pass', 'wf_task_status', '', 'success', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '自动审批'); +INSERT INTO sys_dict_data VALUES (1761600000000000056, 4, '抄送', 'copy', 'wf_task_status', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '抄送'); INSERT INTO sys_dict_data VALUES (1761600000000000052, 5, '退回', 'back', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '退回'); -INSERT INTO sys_dict_data VALUES (1761600000000000053, 6, '终止', 'termination', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '终止'); -INSERT INTO sys_dict_data VALUES (1761600000000000054, 7, '转办', 'transfer', 'wf_task_status', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '转办'); -INSERT INTO sys_dict_data VALUES (1761600000000000055, 8, '委托', 'depute', 'wf_task_status', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '委托'); -INSERT INTO sys_dict_data VALUES (1761600000000000056, 9, '抄送', 'copy', 'wf_task_status', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '抄送'); -INSERT INTO sys_dict_data VALUES (1761600000000000057, 10, '加签', 'sign', 'wf_task_status', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '加签'); -INSERT INTO sys_dict_data VALUES (1761600000000000058, 11, '减签', 'sign_off', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '减签'); -INSERT INTO sys_dict_data VALUES (1761600000000000059, 11, '超时', 'timeout', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '超时'); +INSERT INTO sys_dict_data VALUES (1761600000000000062, 6, '拿回', 'take_back', 'wf_task_status', '', 'warning', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '拿回'); +INSERT INTO sys_dict_data VALUES (1761600000000000051, 7, '失效', 'invalid', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '失效'); +INSERT INTO sys_dict_data VALUES (1761600000000000048, 8, '撤销', 'cancel', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '撤销'); +INSERT INTO sys_dict_data VALUES (1761600000000000064, 9, '转办', 'transfer', 'wf_task_status', '', 'warning', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '转办'); +INSERT INTO sys_dict_data VALUES (1761600000000000065, 10, '委派', 'delegate', 'wf_task_status', '', 'warning', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '委派'); +INSERT INTO sys_dict_data VALUES (1761600000000000066, 11, '加签', 'add_signer', 'wf_task_status', '', 'warning', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '加签'); +INSERT INTO sys_dict_data VALUES (1761600000000000067, 12, '减签', 'remove_signer', 'wf_task_status', '', 'warning', 'N', 1761000000000000103, 1761100000000000001, SYSDATE, NULL, NULL, '减签'); diff --git a/script/sql/postgres/postgres_ry_workflow.sql b/script/sql/postgres/postgres_ry_workflow.sql index 61012fe0e..610dba2bf 100644 --- a/script/sql/postgres/postgres_ry_workflow.sql +++ b/script/sql/postgres/postgres_ry_workflow.sql @@ -13,6 +13,7 @@ CREATE TABLE flow_definition listener_type varchar(100) NULL, listener_path varchar(400) NULL, ext varchar(500) NULL, + def_config text NULL, create_time timestamp NULL, create_by varchar(64) NULL DEFAULT '':: character varying, update_time timestamp NULL, @@ -36,6 +37,7 @@ COMMENT ON COLUMN flow_definition.activity_status IS '流程激活状态(0挂 COMMENT ON COLUMN flow_definition.listener_type IS '监听器类型'; COMMENT ON COLUMN flow_definition.listener_path IS '监听器路径'; COMMENT ON COLUMN flow_definition.ext IS '扩展字段,预留给业务系统使用'; +COMMENT ON COLUMN flow_definition.def_config IS '流程定义配置(自动审批等引擎配置与自定义扩展属性json)'; COMMENT ON COLUMN flow_definition.create_time IS '创建时间'; COMMENT ON COLUMN flow_definition.create_by IS '创建人'; COMMENT ON COLUMN flow_definition.update_time IS '更新时间'; @@ -58,12 +60,16 @@ CREATE TABLE flow_node listener_path varchar(400) NULL, form_custom bpchar(1) NULL DEFAULT 'N':: character varying, form_path varchar(100) NULL, - "version" varchar(20) NOT NULL, create_time timestamp NULL, create_by varchar(64) NULL DEFAULT '':: character varying, update_time timestamp NULL, update_by varchar(64) NULL DEFAULT '':: character varying, ext text NULL, + node_config text NULL, + event_type varchar(40) NULL, + event_name varchar(100) NULL, + attached_to_node_code varchar(100) NULL, + interrupting int2 NULL, del_flag bpchar(1) NULL DEFAULT '0':: character varying, tenant_id varchar(40) NULL, CONSTRAINT flow_node_pkey PRIMARY KEY (id) @@ -83,12 +89,16 @@ COMMENT ON COLUMN flow_node.listener_type IS '监听器类型'; COMMENT ON COLUMN flow_node.listener_path IS '监听器路径'; COMMENT ON COLUMN flow_node.form_custom IS '审批表单是否自定义(Y是 N否)'; COMMENT ON COLUMN flow_node.form_path IS '审批表单路径'; -COMMENT ON COLUMN flow_node."version" IS '版本'; COMMENT ON COLUMN flow_node.create_time IS '创建时间'; COMMENT ON COLUMN flow_node.create_by IS '创建人'; COMMENT ON COLUMN flow_node.update_time IS '更新时间'; COMMENT ON COLUMN flow_node.update_by IS '更新人'; COMMENT ON COLUMN flow_node.ext IS '节点扩展属性'; +COMMENT ON COLUMN flow_node.node_config IS '节点配置(服务任务、事件等引擎节点配置json)'; +COMMENT ON COLUMN flow_node.event_type IS '事件类型(MESSAGE_START TIMER_START TIMER_CATCH MESSAGE_CATCH MESSAGE_THROW TIMER_BOUNDARY MESSAGE_BOUNDARY ERROR_CATCH ERROR_BOUNDARY TERMINATE_END MESSAGE_END)'; +COMMENT ON COLUMN flow_node.event_name IS '事件名称(消息事件使用)'; +COMMENT ON COLUMN flow_node.attached_to_node_code IS '边界事件附着的任务节点编码'; +COMMENT ON COLUMN flow_node.interrupting IS '边界事件是否中断型'; COMMENT ON COLUMN flow_node.del_flag IS '删除标志'; COMMENT ON COLUMN flow_node.tenant_id IS '租户id'; @@ -138,7 +148,7 @@ CREATE TABLE flow_instance definition_id int8 NOT NULL, business_id varchar(40) NOT NULL, node_type int2 NOT NULL, - node_code varchar(40) NOT NULL, + node_code varchar(100) NOT NULL, node_name varchar(100) NULL, variable text NULL, flow_status varchar(20) NOT NULL, @@ -162,7 +172,7 @@ COMMENT ON COLUMN flow_instance.node_type IS '节点类型(0开始节点 1中 COMMENT ON COLUMN flow_instance.node_code IS '流程节点编码'; COMMENT ON COLUMN flow_instance.node_name IS '流程节点名称'; COMMENT ON COLUMN flow_instance.variable IS '任务变量'; -COMMENT ON COLUMN flow_instance.flow_status IS '流程状态(0待提交 1审批中 2审批通过 4终止 5作废 6撤销 8已完成 9已退回 10失效 11拿回)'; +COMMENT ON COLUMN flow_instance.flow_status IS '实例状态(draft待提交 waiting审批中 termination终止 invalid作废 cancel撤销 finish已完成 back已退回 restart重启)'; COMMENT ON COLUMN flow_instance.activity_status IS '流程激活状态(0挂起 1激活)'; COMMENT ON COLUMN flow_instance.def_json IS '流程定义json'; COMMENT ON COLUMN flow_instance.create_time IS '创建时间'; @@ -200,7 +210,7 @@ COMMENT ON COLUMN flow_task.instance_id IS '对应flow_instance表的id'; COMMENT ON COLUMN flow_task.node_code IS '节点编码'; COMMENT ON COLUMN flow_task.node_name IS '节点名称'; COMMENT ON COLUMN flow_task.node_type IS '节点类型(0开始节点 1中间节点 2结束节点 3互斥网关 4并行网关)'; -COMMENT ON COLUMN flow_task.flow_status IS '流程状态(0待提交 1审批中 2审批通过 4终止 5作废 6撤销 8已完成 9已退回 10失效 11拿回)'; +COMMENT ON COLUMN flow_task.flow_status IS '任务状态(pending待审批 pass通过 auto_pass自动审批 back退回 invalid失效 take_back拿回 copy抄送;任务结果,勿套用实例状态编码)'; COMMENT ON COLUMN flow_task.form_custom IS '审批表单是否自定义(Y是 N否)'; COMMENT ON COLUMN flow_task.form_path IS '审批表单路径'; COMMENT ON COLUMN flow_task.create_time IS '创建时间'; @@ -222,15 +232,16 @@ CREATE TABLE flow_his_task target_node_code varchar(200) NULL, target_node_name varchar(200) NULL, approver varchar(40) NULL, - cooperate_type int2 NOT NULL DEFAULT 0, + cooperate_type varchar(20) NOT NULL DEFAULT 'none', collaborator varchar(500) NULL, - skip_type varchar(10) NULL, + skip_type varchar(10) NOT NULL, flow_status varchar(20) NOT NULL, form_custom bpchar(1) NULL DEFAULT 'N':: character varying, form_path varchar(100) NULL, - ext text NULL, message varchar(500) NULL, variable text NULL, + ext text NULL, + node_config text NULL, create_time timestamp NULL, update_time timestamp NULL, del_flag bpchar(1) NULL DEFAULT '0':: character varying, @@ -249,15 +260,16 @@ COMMENT ON COLUMN flow_his_task.node_type IS '开始节点类型(0开始节点 COMMENT ON COLUMN flow_his_task.target_node_code IS '目标节点编码'; COMMENT ON COLUMN flow_his_task.target_node_name IS '结束节点名称'; COMMENT ON COLUMN flow_his_task.approver IS '审批者'; -COMMENT ON COLUMN flow_his_task.cooperate_type IS '协作方式(1审批 2转办 3委派 4会签 5票签 6加签 7减签)'; +COMMENT ON COLUMN flow_his_task.cooperate_type IS '协作方式(none无 transfer转办 delegate委派 add_signer加签 remove_signer减签;行为维度,抄送/会签/票签不落此列,结果见 flow_status)'; COMMENT ON COLUMN flow_his_task.collaborator IS '协作人'; COMMENT ON COLUMN flow_his_task.skip_type IS '流转类型(PASS通过 REJECT退回 NONE无动作)'; -COMMENT ON COLUMN flow_his_task.flow_status IS '流程状态(0待提交 1审批中 2审批通过 4终止 5作废 6撤销 8已完成 9已退回 10失效 11拿回)'; +COMMENT ON COLUMN flow_his_task.flow_status IS '任务状态(pending待审批 pass通过 auto_pass自动审批 back退回 invalid失效 take_back拿回 copy抄送;任务结果,勿套用实例状态编码,唯一例外是撤回归档写 cancel)'; COMMENT ON COLUMN flow_his_task.form_custom IS '审批表单是否自定义(Y是 N否)'; COMMENT ON COLUMN flow_his_task.form_path IS '审批表单路径'; COMMENT ON COLUMN flow_his_task.message IS '审批意见'; COMMENT ON COLUMN flow_his_task.variable IS '任务变量'; COMMENT ON COLUMN flow_his_task.ext IS '扩展字段,预留给业务系统使用'; +COMMENT ON COLUMN flow_his_task.node_config IS '节点配置(服务任务等引擎配置与自定义扩展属性json)'; COMMENT ON COLUMN flow_his_task.create_time IS '任务开始时间'; COMMENT ON COLUMN flow_his_task.update_time IS '审批完成时间'; COMMENT ON COLUMN flow_his_task.del_flag IS '删除标志'; @@ -270,7 +282,7 @@ CREATE TABLE flow_user processed_by varchar(80) NULL, associated int8 NOT NULL, create_time timestamp NULL, - create_by varchar(64) NULL DEFAULT '':: character varying, + create_by varchar(80) NULL:: character varying, update_time timestamp NULL, update_by varchar(64) NULL DEFAULT '':: character varying, del_flag bpchar(1) NULL DEFAULT '0':: character varying, @@ -282,9 +294,9 @@ CREATE INDEX user_associated_idx ON FLOW_USER USING btree (associated); COMMENT ON TABLE flow_user IS '流程用户表'; COMMENT ON COLUMN flow_user.id IS '主键id'; -COMMENT ON COLUMN flow_user."type" IS '人员类型(1待办任务的审批人权限 2待办任务的转办人权限 3待办任务的委托人权限)'; +COMMENT ON COLUMN flow_user."type" IS '人员类型(1待办任务的审批人权限 2待办任务的转办人权限 3待办任务的委托人权限 4待办任务的抄送人)'; COMMENT ON COLUMN flow_user.processed_by IS '权限人'; -COMMENT ON COLUMN flow_user.associated IS '任务表id'; +COMMENT ON COLUMN flow_user.associated IS '关联id(待办任务id;抄送人记录为 flow_his_task.id)'; COMMENT ON COLUMN flow_user.create_time IS '创建时间'; COMMENT ON COLUMN flow_user.create_by IS '创建人'; COMMENT ON COLUMN flow_user.update_time IS '更新时间'; @@ -292,6 +304,75 @@ COMMENT ON COLUMN flow_user.update_by IS '更新人'; COMMENT ON COLUMN flow_user.del_flag IS '删除标志'; COMMENT ON COLUMN flow_user.tenant_id IS '租户id'; +-- ---------------------------- +-- 事件等待表 +-- ---------------------------- +CREATE TABLE flow_event_wait +( + id int8 NOT NULL, + tenant_id varchar(40) NULL, + del_flag bpchar(1) NULL DEFAULT '0':: character varying, + definition_id int8 NULL, + instance_id int8 NULL, + node_code varchar(100) NULL, + event_type varchar(40) NOT NULL, + event_name varchar(100) NULL, + attached_task_id int8 NULL, + trigger_type varchar(40) NULL, + trigger_interval varchar(100) NULL, + next_trigger_at timestamp NULL, + timezone varchar(40) NULL, + repeat_count int4 NULL, + max_repeat_count int4 NULL, + status varchar(20) NOT NULL, + retry_count int4 NULL, + max_retry_count int4 NULL, + retry_at timestamp NULL, + lock_owner varchar(64) NULL, + lock_until timestamp NULL, + version int4 NULL, + payload text NULL, + last_error varchar(500) NULL, + create_time timestamp NULL, + update_time timestamp NULL, + CONSTRAINT flow_event_wait_pkey PRIMARY KEY (id) +); +CREATE INDEX event_wait_status_next ON flow_event_wait USING btree (status, next_trigger_at, id); +CREATE INDEX event_wait_status_retry ON flow_event_wait USING btree (status, retry_at, id); +CREATE INDEX event_wait_tenant_status_next ON flow_event_wait USING btree (tenant_id, status, next_trigger_at, id); +CREATE INDEX event_wait_tenant_status_retry ON flow_event_wait USING btree (tenant_id, status, retry_at, id); +CREATE INDEX event_wait_instance_task ON flow_event_wait USING btree (instance_id, attached_task_id); +CREATE INDEX event_wait_name_instance_status ON flow_event_wait USING btree (event_name, instance_id, status); +CREATE INDEX event_wait_def_node_status ON flow_event_wait USING btree (definition_id, node_code, status); +COMMENT ON TABLE flow_event_wait IS '事件等待表'; + +COMMENT ON COLUMN flow_event_wait.id IS '主键id'; +COMMENT ON COLUMN flow_event_wait.tenant_id IS '租户id'; +COMMENT ON COLUMN flow_event_wait.del_flag IS '删除标志'; +COMMENT ON COLUMN flow_event_wait.definition_id IS '流程定义id(定时启动事件必须有)'; +COMMENT ON COLUMN flow_event_wait.instance_id IS '流程实例id(实例等待事件必须有)'; +COMMENT ON COLUMN flow_event_wait.node_code IS '事件节点编码(定位沿用 definition_id + node_code)'; +COMMENT ON COLUMN flow_event_wait.event_type IS '事件类型(MESSAGE_START TIMER_START TIMER_CATCH MESSAGE_CATCH MESSAGE_THROW TIMER_BOUNDARY MESSAGE_BOUNDARY ERROR_CATCH ERROR_BOUNDARY TERMINATE_END MESSAGE_END)'; +COMMENT ON COLUMN flow_event_wait.event_name IS '事件名称(消息事件使用)'; +COMMENT ON COLUMN flow_event_wait.attached_task_id IS '边界事件关联的待办任务id'; +COMMENT ON COLUMN flow_event_wait.trigger_type IS '触发策略类型(AFTER FIXED POINT_IN_TIME)'; +COMMENT ON COLUMN flow_event_wait.trigger_interval IS '触发策略原始配置'; +COMMENT ON COLUMN flow_event_wait.next_trigger_at IS '下次触发时间'; +COMMENT ON COLUMN flow_event_wait.timezone IS '定时触发使用的时区'; +COMMENT ON COLUMN flow_event_wait.repeat_count IS '已触发次数'; +COMMENT ON COLUMN flow_event_wait.max_repeat_count IS '最大触发次数'; +COMMENT ON COLUMN flow_event_wait.status IS '等待状态(WAITING RUNNING CANCELLED RETRY DEAD)'; +COMMENT ON COLUMN flow_event_wait.retry_count IS '已重试次数'; +COMMENT ON COLUMN flow_event_wait.max_retry_count IS '最大重试次数'; +COMMENT ON COLUMN flow_event_wait.retry_at IS '下次重试时间'; +COMMENT ON COLUMN flow_event_wait.lock_owner IS '抢占持有者标识'; +COMMENT ON COLUMN flow_event_wait.lock_until IS '抢占租约到期时间'; +COMMENT ON COLUMN flow_event_wait.version IS '乐观锁版本号'; +COMMENT ON COLUMN flow_event_wait.payload IS '消息事件接收的业务载荷'; +COMMENT ON COLUMN flow_event_wait.last_error IS '最近一次失败原因摘要'; +COMMENT ON COLUMN flow_event_wait.create_time IS '创建时间'; +COMMENT ON COLUMN flow_event_wait.update_time IS '更新时间'; + -- ---------------------------- -- 流程分类表 -- ---------------------------- @@ -488,6 +569,11 @@ INSERT INTO sys_menu VALUES (1761400000000011804, '流程spel表达式定义修 INSERT INTO sys_menu VALUES (1761400000000011805, '流程spel表达式定义删除', 1761400000000011801, 4, '#', '', NULL, 'N', 'Y', 'F', '0', '0', 'workflow:spel:remove', '#', '', '', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, ''); INSERT INTO sys_menu VALUES (1761400000000011806, '流程spel表达式定义导出', 1761400000000011801, 5, '#', '', NULL, 'N', 'Y', 'F', '0', '0', 'workflow:spel:export', '#', '', '', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, ''); +-- 定时任务(事件等待记录)菜单与按钮 +INSERT INTO sys_menu VALUES (1761400000000011901, '定时任务', 1761400000000011630, 3, 'timer', 'workflow/timer/index', '', 'N', 'N', 'C', '0', '0', 'workflow:eventWait:list', 'time', '', '', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '定时任务与事件等待监控菜单'); +INSERT INTO sys_menu VALUES (1761400000000011902, '定时任务查询', 1761400000000011901, 1, '#', '', NULL, 'N', 'Y', 'F', '0', '0', 'workflow:eventWait:query', '#', '', '', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, ''); +INSERT INTO sys_menu VALUES (1761400000000011903, '定时任务手动补发', 1761400000000011901, 2, '#', '', NULL, 'N', 'Y', 'F', '0', '0', 'workflow:eventWait:trigger', '#', '', '', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, ''); + INSERT INTO sys_menu VALUES (1761400000000011638, '请假申请', 1761400000000000005, 1, 'leave', 'workflow/leave/index', '', 'N', 'Y', 'C', '0', '0', 'workflow:leave:list', '#', '', '', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '请假申请菜单'); INSERT INTO sys_menu VALUES (1761400000000011639, '请假申请查询', 1761400000000011638, 1, '#', '', '', 'N', 'Y', 'F', '0', '0', 'workflow:leave:query', '#', '', '', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, ''); INSERT INTO sys_menu VALUES (1761400000000011640, '请假申请新增', 1761400000000011638, 2, '#', '', '', 'N', 'Y', 'F', '0', '0', 'workflow:leave:add', '#', '', '', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, ''); @@ -505,18 +591,19 @@ INSERT INTO sys_dict_data VALUES (1761600000000000042, 4, '已完成', 'finish', INSERT INTO sys_dict_data VALUES (1761600000000000043, 5, '已作废', 'invalid', 'wf_business_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '已作废'); INSERT INTO sys_dict_data VALUES (1761600000000000044, 6, '已退回', 'back', 'wf_business_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '已退回'); INSERT INTO sys_dict_data VALUES (1761600000000000045, 7, '已终止', 'termination', 'wf_business_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '已终止'); +INSERT INTO sys_dict_data VALUES (1761600000000000063, 8, '重启', 'restart', 'wf_business_status', '', 'info', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '重启'); INSERT INTO sys_dict_data VALUES (1761600000000000046, 1, '自定义表单', 'static', 'wf_form_type', '', 'success', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '自定义表单'); INSERT INTO sys_dict_data VALUES (1761600000000000047, 2, '动态表单', 'dynamic', 'wf_form_type', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '动态表单'); -INSERT INTO sys_dict_data VALUES (1761600000000000048, 1, '撤销', 'cancel', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '撤销'); +INSERT INTO sys_dict_data VALUES (1761600000000000060, 1, '待审批', 'pending', 'wf_task_status', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '待审批'); INSERT INTO sys_dict_data VALUES (1761600000000000049, 2, '通过', 'pass', 'wf_task_status', '', 'success', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '通过'); -INSERT INTO sys_dict_data VALUES (1761600000000000050, 3, '待审核', 'waiting', 'wf_task_status', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '待审核'); -INSERT INTO sys_dict_data VALUES (1761600000000000051, 4, '作废', 'invalid', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '作废'); +INSERT INTO sys_dict_data VALUES (1761600000000000061, 3, '自动审批', 'auto_pass', 'wf_task_status', '', 'success', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '自动审批'); +INSERT INTO sys_dict_data VALUES (1761600000000000056, 4, '抄送', 'copy', 'wf_task_status', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '抄送'); INSERT INTO sys_dict_data VALUES (1761600000000000052, 5, '退回', 'back', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '退回'); -INSERT INTO sys_dict_data VALUES (1761600000000000053, 6, '终止', 'termination', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '终止'); -INSERT INTO sys_dict_data VALUES (1761600000000000054, 7, '转办', 'transfer', 'wf_task_status', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '转办'); -INSERT INTO sys_dict_data VALUES (1761600000000000055, 8, '委托', 'depute', 'wf_task_status', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '委托'); -INSERT INTO sys_dict_data VALUES (1761600000000000056, 9, '抄送', 'copy', 'wf_task_status', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '抄送'); -INSERT INTO sys_dict_data VALUES (1761600000000000057, 10, '加签', 'sign', 'wf_task_status', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '加签'); -INSERT INTO sys_dict_data VALUES (1761600000000000058, 11, '减签', 'sign_off', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '减签'); -INSERT INTO sys_dict_data VALUES (1761600000000000059, 11, '超时', 'timeout', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '超时'); +INSERT INTO sys_dict_data VALUES (1761600000000000062, 6, '拿回', 'take_back', 'wf_task_status', '', 'warning', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '拿回'); +INSERT INTO sys_dict_data VALUES (1761600000000000051, 7, '失效', 'invalid', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '失效'); +INSERT INTO sys_dict_data VALUES (1761600000000000048, 8, '撤销', 'cancel', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '撤销'); +INSERT INTO sys_dict_data VALUES (1761600000000000064, 9, '转办', 'transfer', 'wf_task_status', '', 'warning', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '转办'); +INSERT INTO sys_dict_data VALUES (1761600000000000065, 10, '委派', 'delegate', 'wf_task_status', '', 'warning', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '委派'); +INSERT INTO sys_dict_data VALUES (1761600000000000066, 11, '加签', 'add_signer', 'wf_task_status', '', 'warning', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '加签'); +INSERT INTO sys_dict_data VALUES (1761600000000000067, 12, '减签', 'remove_signer', 'wf_task_status', '', 'warning', 'N', 1761000000000000103, 1761100000000000001, now(), NULL, NULL, '减签'); diff --git a/script/sql/ry_workflow.sql b/script/sql/ry_workflow.sql index 581395dc3..64b7c18f5 100644 --- a/script/sql/ry_workflow.sql +++ b/script/sql/ry_workflow.sql @@ -16,6 +16,7 @@ CREATE TABLE `flow_definition` `listener_type` varchar(100) DEFAULT NULL COMMENT '监听器类型', `listener_path` varchar(400) DEFAULT NULL COMMENT '监听器路径', `ext` varchar(500) DEFAULT NULL COMMENT '业务详情 存业务表对象json字符串', + `def_config` text COMMENT '流程定义配置(自动审批等引擎配置与自定义扩展属性json)', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `create_by` varchar(64) DEFAULT '' COMMENT '创建人', `update_time` datetime DEFAULT NULL COMMENT '更新时间', @@ -40,12 +41,16 @@ CREATE TABLE `flow_node` `listener_path` varchar(400) DEFAULT NULL COMMENT '监听器路径', `form_custom` char(1) DEFAULT 'N' COMMENT '审批表单是否自定义(Y是 N否)', `form_path` varchar(100) DEFAULT NULL COMMENT '审批表单路径', - `version` varchar(20) NOT NULL COMMENT '版本', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `create_by` varchar(64) DEFAULT '' COMMENT '创建人', `update_time` datetime DEFAULT NULL COMMENT '更新时间', `update_by` varchar(64) DEFAULT '' COMMENT '更新人', `ext` text COMMENT '节点扩展属性', + `node_config` text COMMENT '节点配置(服务任务、事件等引擎节点配置json)', + `event_type` varchar(40) DEFAULT NULL COMMENT '事件类型(MESSAGE_START TIMER_START TIMER_CATCH MESSAGE_CATCH MESSAGE_THROW TIMER_BOUNDARY MESSAGE_BOUNDARY ERROR_CATCH ERROR_BOUNDARY TERMINATE_END MESSAGE_END)', + `event_name` varchar(100) DEFAULT NULL COMMENT '事件名称(消息事件使用)', + `attached_to_node_code` varchar(100) DEFAULT NULL COMMENT '边界事件附着的任务节点编码', + `interrupting` tinyint(1) DEFAULT NULL COMMENT '边界事件是否中断型', `del_flag` char(1) DEFAULT '0' COMMENT '删除标志', `tenant_id` varchar(40) DEFAULT NULL COMMENT '租户id', PRIMARY KEY (`id`) USING BTREE @@ -78,10 +83,10 @@ CREATE TABLE `flow_instance` `definition_id` bigint NOT NULL COMMENT '对应flow_definition表的id', `business_id` varchar(40) NOT NULL COMMENT '业务id', `node_type` tinyint(1) NOT NULL COMMENT '节点类型(0开始节点 1中间节点 2结束节点 3互斥网关 4并行网关)', - `node_code` varchar(40) NOT NULL COMMENT '流程节点编码', + `node_code` varchar(100) NOT NULL COMMENT '流程节点编码', `node_name` varchar(100) DEFAULT NULL COMMENT '流程节点名称', `variable` text COMMENT '任务变量', - `flow_status` varchar(20) NOT NULL COMMENT '流程状态(0待提交 1审批中 2审批通过 4终止 5作废 6撤销 8已完成 9已退回 10失效 11拿回)', + `flow_status` varchar(20) NOT NULL COMMENT '实例状态(draft待提交 waiting审批中 termination终止 invalid作废 cancel撤销 finish已完成 back已退回 restart重启)', `activity_status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '流程激活状态(0挂起 1激活)', `def_json` text COMMENT '流程定义json', `create_time` datetime DEFAULT NULL COMMENT '创建时间', @@ -102,7 +107,7 @@ CREATE TABLE `flow_task` `node_code` varchar(100) NOT NULL COMMENT '节点编码', `node_name` varchar(100) DEFAULT NULL COMMENT '节点名称', `node_type` tinyint(1) NOT NULL COMMENT '节点类型(0开始节点 1中间节点 2结束节点 3互斥网关 4并行网关)', - `flow_status` varchar(20) NOT NULL COMMENT '流程状态(0待提交 1审批中 2审批通过 4终止 5作废 6撤销 8已完成 9已退回 10失效 11拿回)', + `flow_status` varchar(20) NOT NULL COMMENT '任务状态(pending待审批 pass通过 auto_pass自动审批 back退回 invalid失效 take_back拿回 copy抄送;任务结果,勿套用实例状态编码)', `form_custom` char(1) DEFAULT 'N' COMMENT '审批表单是否自定义(Y是 N否)', `form_path` varchar(100) DEFAULT NULL COMMENT '审批表单路径', `create_time` datetime DEFAULT NULL COMMENT '创建时间', @@ -126,15 +131,16 @@ CREATE TABLE `flow_his_task` `target_node_code` varchar(200) DEFAULT NULL COMMENT '目标节点编码', `target_node_name` varchar(200) DEFAULT NULL COMMENT '结束节点名称', `approver` varchar(40) DEFAULT NULL COMMENT '审批人', - `cooperate_type` tinyint(1) NOT NULL DEFAULT '0' COMMENT '协作方式(1审批 2转办 3委派 4会签 5票签 6加签 7减签)', + `cooperate_type` varchar(20) NOT NULL DEFAULT 'none' COMMENT '协作方式(none无 transfer转办 delegate委派 add_signer加签 remove_signer减签;行为维度,抄送/会签/票签不落此列,结果见 flow_status)', `collaborator` varchar(500) DEFAULT NULL COMMENT '协作人', `skip_type` varchar(10) NOT NULL COMMENT '流转类型(PASS通过 REJECT退回 NONE无动作)', - `flow_status` varchar(20) NOT NULL COMMENT '流程状态(0待提交 1审批中 2审批通过 4终止 5作废 6撤销 8已完成 9已退回 10失效 11拿回)', + `flow_status` varchar(20) NOT NULL COMMENT '任务状态(pending待审批 pass通过 auto_pass自动审批 back退回 invalid失效 take_back拿回 copy抄送;任务结果,勿套用实例状态编码,唯一例外是撤回归档写 cancel)', `form_custom` char(1) DEFAULT 'N' COMMENT '审批表单是否自定义(Y是 N否)', `form_path` varchar(100) DEFAULT NULL COMMENT '审批表单路径', `message` varchar(500) DEFAULT NULL COMMENT '审批意见', `variable` TEXT DEFAULT NULL COMMENT '任务变量', `ext` TEXT DEFAULT NULL COMMENT '业务详情 存业务表对象json字符串', + `node_config` TEXT DEFAULT NULL COMMENT '节点配置(服务任务等引擎配置与自定义扩展属性json)', `create_time` datetime DEFAULT NULL COMMENT '任务开始时间', `update_time` datetime DEFAULT NULL COMMENT '审批完成时间', `del_flag` char(1) DEFAULT '0' COMMENT '删除标志', @@ -146,9 +152,9 @@ CREATE TABLE `flow_his_task` CREATE TABLE `flow_user` ( `id` bigint NOT NULL COMMENT '主键id', - `type` char(1) NOT NULL COMMENT '人员类型(1待办任务的审批人权限 2待办任务的转办人权限 3待办任务的委托人权限)', + `type` char(1) NOT NULL COMMENT '人员类型(1待办任务的审批人权限 2待办任务的转办人权限 3待办任务的委托人权限 4待办任务的抄送人)', `processed_by` varchar(80) DEFAULT NULL COMMENT '权限人', - `associated` bigint NOT NULL COMMENT '任务表id', + `associated` bigint NOT NULL COMMENT '关联id(待办任务id;抄送人记录为 flow_his_task.id)', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `create_by` varchar(80) DEFAULT NULL COMMENT '创建人', `update_time` datetime DEFAULT NULL COMMENT '更新时间', @@ -160,6 +166,46 @@ CREATE TABLE `flow_user` KEY `user_associated` (`associated`) USING BTREE ) ENGINE = InnoDB COMMENT ='流程用户表'; +-- 事件等待表:定时 / 消息等需要“等待”的事件统一落在此表,状态独立于 flow_status。 +-- 事件能力默认关闭(warm-flow.timer-enabled=false),未开启时引擎不扫描本表。 +CREATE TABLE `flow_event_wait` +( + `id` bigint NOT NULL COMMENT '主键id', + `tenant_id` varchar(40) DEFAULT NULL COMMENT '租户id', + `del_flag` char(1) DEFAULT '0' COMMENT '删除标志', + `definition_id` bigint DEFAULT NULL COMMENT '流程定义id(定时启动事件必须有)', + `instance_id` bigint DEFAULT NULL COMMENT '流程实例id(实例等待事件必须有)', + `node_code` varchar(100) DEFAULT NULL COMMENT '事件节点编码(定位沿用 definition_id + node_code)', + `event_type` varchar(40) NOT NULL COMMENT '事件类型(MESSAGE_START TIMER_START TIMER_CATCH MESSAGE_CATCH MESSAGE_THROW TIMER_BOUNDARY MESSAGE_BOUNDARY ERROR_CATCH ERROR_BOUNDARY TERMINATE_END MESSAGE_END)', + `event_name` varchar(100) DEFAULT NULL COMMENT '事件名称(消息事件使用)', + `attached_task_id` bigint DEFAULT NULL COMMENT '边界事件关联的待办任务id', + `trigger_type` varchar(40) DEFAULT NULL COMMENT '触发策略类型(AFTER FIXED POINT_IN_TIME)', + `trigger_interval` varchar(100) DEFAULT NULL COMMENT '触发策略原始配置', + `next_trigger_at` datetime DEFAULT NULL COMMENT '下次触发时间', + `timezone` varchar(40) DEFAULT NULL COMMENT '定时触发使用的时区', + `repeat_count` int DEFAULT NULL COMMENT '已触发次数', + `max_repeat_count` int DEFAULT NULL COMMENT '最大触发次数', + `status` varchar(20) NOT NULL COMMENT '等待状态(WAITING RUNNING CANCELLED RETRY DEAD)', + `retry_count` int DEFAULT NULL COMMENT '已重试次数', + `max_retry_count` int DEFAULT NULL COMMENT '最大重试次数', + `retry_at` datetime DEFAULT NULL COMMENT '下次重试时间', + `lock_owner` varchar(64) DEFAULT NULL COMMENT '抢占持有者标识', + `lock_until` datetime DEFAULT NULL COMMENT '抢占租约到期时间', + `version` int DEFAULT NULL COMMENT '乐观锁版本号', + `payload` text COMMENT '消息事件接收的业务载荷', + `last_error` varchar(500) DEFAULT NULL COMMENT '最近一次失败原因摘要', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `event_wait_status_next` (`status`, `next_trigger_at`, `id`) USING BTREE, + KEY `event_wait_status_retry` (`status`, `retry_at`, `id`) USING BTREE, + KEY `event_wait_tenant_status_next` (`tenant_id`, `status`, `next_trigger_at`, `id`) USING BTREE, + KEY `event_wait_tenant_status_retry` (`tenant_id`, `status`, `retry_at`, `id`) USING BTREE, + KEY `event_wait_instance_task` (`instance_id`, `attached_task_id`) USING BTREE, + KEY `event_wait_name_instance_status` (`event_name`, `instance_id`, `status`) USING BTREE, + KEY `event_wait_def_node_status` (`definition_id`, `node_code`, `status`) USING BTREE +) ENGINE = InnoDB COMMENT ='事件等待表'; + -- ---------------------------- -- 流程分类表 -- ---------------------------- @@ -303,6 +349,11 @@ INSERT INTO sys_menu VALUES (1761400000000011803, '流程达式定义新增', 17 INSERT INTO sys_menu VALUES (1761400000000011804, '流程达式定义修改', 1761400000000011801, 3, '#', '', NULL, 'N', 'Y', 'F', '0', '0', 'workflow:spel:edit', '#', '', '', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, ''); INSERT INTO sys_menu VALUES (1761400000000011805, '流程达式定义删除', 1761400000000011801, 4, '#', '', NULL, 'N', 'Y', 'F', '0', '0', 'workflow:spel:remove', '#', '', '', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, ''); INSERT INTO sys_menu VALUES (1761400000000011806, '流程达式定义导出', 1761400000000011801, 5, '#', '', NULL, 'N', 'Y', 'F', '0', '0', 'workflow:spel:export', '#', '', '', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, ''); + +-- 定时任务(事件等待记录)菜单与按钮 +insert into sys_menu values (1761400000000011901, '定时任务', 1761400000000011630, 3, 'timer', 'workflow/timer/index', '', 'N', 'N', 'C', '0', '0', 'workflow:eventWait:list', 'time', '', '', 1761000000000000103, 1761100000000000001, sysdate(), null, null, '定时任务与事件等待监控菜单'); +insert into sys_menu values (1761400000000011902, '定时任务查询', 1761400000000011901, 1, '#', '', '', 'N', 'Y', 'F', '0', '0', 'workflow:eventWait:query', '#', '', '', 1761000000000000103, 1761100000000000001, sysdate(), null, null, ''); +insert into sys_menu values (1761400000000011903, '定时任务手动补发', 1761400000000011901, 2, '#', '', '', 'N', 'Y', 'F', '0', '0', 'workflow:eventWait:trigger', '#', '', '', 1761000000000000103, 1761100000000000001, sysdate(), null, null, ''); -- 请假测试相关按钮 insert into sys_menu VALUES (1761400000000011638, '请假申请', 1761400000000000005, 1, 'leave', 'workflow/leave/index', '', 'N', 'Y', 'C', '0', '0', 'workflow:leave:list', '#', '', '', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '请假申请菜单'); insert into sys_menu VALUES (1761400000000011639, '请假申请查询', 1761400000000011638, 1, '#', '', '', 'N', 'Y', 'F', '0', '0', 'workflow:leave:query', '#', '', '', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, ''); @@ -321,18 +372,20 @@ INSERT INTO sys_dict_data VALUES (1761600000000000042, 4, '已完成', 'finish', INSERT INTO sys_dict_data VALUES (1761600000000000043, 5, '已作废', 'invalid', 'wf_business_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '已作废'); INSERT INTO sys_dict_data VALUES (1761600000000000044, 6, '已退回', 'back', 'wf_business_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '已退回'); INSERT INTO sys_dict_data VALUES (1761600000000000045, 7, '已终止', 'termination', 'wf_business_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '已终止'); +INSERT INTO sys_dict_data VALUES (1761600000000000063, 8, '重启', 'restart', 'wf_business_status', '', 'info', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '重启'); INSERT INTO sys_dict_data VALUES (1761600000000000046, 1, '自定义表单', 'static', 'wf_form_type', '', 'success', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '自定义表单'); INSERT INTO sys_dict_data VALUES (1761600000000000047, 2, '动态表单', 'dynamic', 'wf_form_type', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '动态表单'); -INSERT INTO sys_dict_data VALUES (1761600000000000048, 1, '撤销', 'cancel', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '撤销'); +INSERT INTO sys_dict_data VALUES (1761600000000000060, 1, '待审批', 'pending', 'wf_task_status', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '待审批'); INSERT INTO sys_dict_data VALUES (1761600000000000049, 2, '通过', 'pass', 'wf_task_status', '', 'success', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '通过'); -INSERT INTO sys_dict_data VALUES (1761600000000000050, 3, '待审核', 'waiting', 'wf_task_status', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '待审核'); -INSERT INTO sys_dict_data VALUES (1761600000000000051, 4, '作废', 'invalid', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '作废'); +INSERT INTO sys_dict_data VALUES (1761600000000000061, 3, '自动审批', 'auto_pass', 'wf_task_status', '', 'success', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '自动审批'); +INSERT INTO sys_dict_data VALUES (1761600000000000056, 4, '抄送', 'copy', 'wf_task_status', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '抄送'); INSERT INTO sys_dict_data VALUES (1761600000000000052, 5, '退回', 'back', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '退回'); -INSERT INTO sys_dict_data VALUES (1761600000000000053, 6, '终止', 'termination', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '终止'); -INSERT INTO sys_dict_data VALUES (1761600000000000054, 7, '转办', 'transfer', 'wf_task_status', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '转办'); -INSERT INTO sys_dict_data VALUES (1761600000000000055, 8, '委托', 'depute', 'wf_task_status', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '委托'); -INSERT INTO sys_dict_data VALUES (1761600000000000056, 9, '抄送', 'copy', 'wf_task_status', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '抄送'); -INSERT INTO sys_dict_data VALUES (1761600000000000057, 10, '加签', 'sign', 'wf_task_status', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '加签'); -INSERT INTO sys_dict_data VALUES (1761600000000000058, 11, '减签', 'sign_off', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '减签'); -INSERT INTO sys_dict_data VALUES (1761600000000000059, 11, '超时', 'timeout', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '超时'); +INSERT INTO sys_dict_data VALUES (1761600000000000062, 6, '拿回', 'take_back', 'wf_task_status', '', 'warning', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '拿回'); +INSERT INTO sys_dict_data VALUES (1761600000000000051, 7, '失效', 'invalid', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '失效'); +INSERT INTO sys_dict_data VALUES (1761600000000000048, 8, '撤销', 'cancel', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '撤销'); +INSERT INTO sys_dict_data VALUES (1761600000000000064, 9, '转办', 'transfer', 'wf_task_status', '', 'warning', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '转办'); +INSERT INTO sys_dict_data VALUES (1761600000000000065, 10, '委派', 'delegate', 'wf_task_status', '', 'warning', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '委派'); +INSERT INTO sys_dict_data VALUES (1761600000000000066, 11, '加签', 'add_signer', 'wf_task_status', '', 'warning', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '加签'); +INSERT INTO sys_dict_data VALUES (1761600000000000067, 12, '减签', 'remove_signer', 'wf_task_status', '', 'warning', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '减签'); +INSERT INTO sys_dict_data VALUES (1761600000000000068, 13, '已终止', 'termination', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '终止归档留痕'); diff --git a/script/sql/sqlserver/sqlserver_ry_workflow.sql b/script/sql/sqlserver/sqlserver_ry_workflow.sql index b91e3c81b..711556d45 100644 --- a/script/sql/sqlserver/sqlserver_ry_workflow.sql +++ b/script/sql/sqlserver/sqlserver_ry_workflow.sql @@ -8,17 +8,18 @@ CREATE TABLE flow_definition ( model_value nvarchar(40) DEFAULT('CLASSICS') NOT NULL, category nvarchar(100) NULL, version nvarchar(20) NOT NULL, - is_publish tinyint DEFAULT('0') NULL, + is_publish tinyint DEFAULT('0') NOT NULL, form_custom nchar(1) DEFAULT('N') NULL, form_path nvarchar(100) NULL, - activity_status tinyint DEFAULT('1') NULL, + activity_status tinyint DEFAULT('1') NOT NULL, listener_type nvarchar(100) NULL, listener_path nvarchar(400) NULL, ext nvarchar(500) NULL, + def_config nvarchar(max) NULL, create_time datetime2(7) NULL, - create_by nvarchar(64) NULL, + create_by nvarchar(64) DEFAULT('') NULL, update_time datetime2(7) NULL, - update_by nvarchar(64) NULL, + update_by nvarchar(64) DEFAULT('') NULL, del_flag nchar(1) DEFAULT('0') NULL, tenant_id nvarchar(40) NULL, CONSTRAINT PK__flow_def__3213E83FEE39AE33 PRIMARY KEY CLUSTERED (id) @@ -119,6 +120,13 @@ EXEC sp_addextendedproperty 'COLUMN', N'ext' GO +EXEC sp_addextendedproperty +'MS_Description', N'流程定义配置(自动审批等引擎配置与自定义扩展属性json)', +'SCHEMA', N'dbo', +'TABLE', N'flow_definition', +'COLUMN', N'def_config' +GO + EXEC sp_addextendedproperty 'MS_Description', N'创建时间', 'SCHEMA', N'dbo', @@ -181,12 +189,16 @@ CREATE TABLE flow_node ( listener_path nvarchar(400) NULL, form_custom nchar(1) DEFAULT('N') NULL, form_path nvarchar(100) NULL, - version nvarchar(20) NOT NULL, create_time datetime2(7) NULL, - create_by nvarchar(64) NULL, + create_by nvarchar(64) DEFAULT('') NULL, update_time datetime2(7) NULL, - update_by nvarchar(64) NULL, + update_by nvarchar(64) DEFAULT('') NULL, ext nvarchar(max) NULL, + node_config nvarchar(max) NULL, + event_type nvarchar(40) NULL, + event_name nvarchar(100) NULL, + attached_to_node_code nvarchar(100) NULL, + interrupting tinyint NULL, del_flag nchar(1) DEFAULT('0') NULL, tenant_id nvarchar(40) NULL, CONSTRAINT PK__flow_nod__3213E83F372470DE PRIMARY KEY CLUSTERED (id) @@ -287,13 +299,6 @@ EXEC sp_addextendedproperty 'COLUMN', N'form_path' GO -EXEC sp_addextendedproperty -'MS_Description', N'版本', -'SCHEMA', N'dbo', -'TABLE', N'flow_node', -'COLUMN', N'version' -GO - EXEC sp_addextendedproperty 'MS_Description', N'创建时间', 'SCHEMA', N'dbo', @@ -329,6 +334,41 @@ EXEC sp_addextendedproperty 'COLUMN', N'ext' GO +EXEC sp_addextendedproperty +'MS_Description', N'节点配置(服务任务、事件等引擎节点配置json)', +'SCHEMA', N'dbo', +'TABLE', N'flow_node', +'COLUMN', N'node_config' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'事件类型(MESSAGE_START TIMER_START TIMER_CATCH MESSAGE_CATCH MESSAGE_THROW TIMER_BOUNDARY MESSAGE_BOUNDARY ERROR_CATCH ERROR_BOUNDARY TERMINATE_END MESSAGE_END)', +'SCHEMA', N'dbo', +'TABLE', N'flow_node', +'COLUMN', N'event_type' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'事件名称(消息事件使用)', +'SCHEMA', N'dbo', +'TABLE', N'flow_node', +'COLUMN', N'event_name' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'边界事件附着的任务节点编码', +'SCHEMA', N'dbo', +'TABLE', N'flow_node', +'COLUMN', N'attached_to_node_code' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'边界事件是否中断型', +'SCHEMA', N'dbo', +'TABLE', N'flow_node', +'COLUMN', N'interrupting' +GO + EXEC sp_addextendedproperty 'MS_Description', N'删除标志', @@ -362,9 +402,9 @@ CREATE TABLE flow_skip ( skip_condition nvarchar(200) NULL, coordinate nvarchar(100) NULL, create_time datetime2(7) NULL, - create_by nvarchar(64) NULL, + create_by nvarchar(64) DEFAULT('') NULL, update_time datetime2(7) NULL, - update_by nvarchar(64) NULL, + update_by nvarchar(64) DEFAULT('') NULL, del_flag nchar(1) DEFAULT('0') NULL, tenant_id nvarchar(40) NULL, CONSTRAINT PK__flow_ski__3213E83F073FEE6E PRIMARY KEY CLUSTERED (id) @@ -497,16 +537,16 @@ CREATE TABLE flow_instance ( definition_id bigint NOT NULL, business_id nvarchar(40) NOT NULL, node_type tinyint NOT NULL, - node_code nvarchar(40) NOT NULL, + node_code nvarchar(100) NOT NULL, node_name nvarchar(100) NULL, variable nvarchar(max) NULL, flow_status nvarchar(20) NOT NULL, - activity_status tinyint DEFAULT('1') NULL, + activity_status tinyint DEFAULT('1') NOT NULL, def_json nvarchar(max) NULL, create_time datetime2(7) NULL, - create_by nvarchar(64) NULL, + create_by nvarchar(64) DEFAULT('') NULL, update_time datetime2(7) NULL, - update_by nvarchar(64) NULL, + update_by nvarchar(64) DEFAULT('') NULL, ext nvarchar(500) NULL, del_flag nchar(1) DEFAULT('0') NULL, tenant_id nvarchar(40) NULL, @@ -568,7 +608,7 @@ EXEC sp_addextendedproperty GO EXEC sp_addextendedproperty -'MS_Description', N'流程状态(0待提交 1审批中 2审批通过 4终止 5作废 6撤销 8已完成 9已退回 10失效 11拿回)', +'MS_Description', N'实例状态(draft待提交 waiting审批中 termination终止 invalid作废 cancel撤销 finish已完成 back已退回 restart重启)', 'SCHEMA', N'dbo', 'TABLE', N'flow_instance', 'COLUMN', N'flow_status' @@ -654,9 +694,9 @@ CREATE TABLE flow_task ( form_custom nchar(1) DEFAULT('N') NULL, form_path nvarchar(100) NULL, create_time datetime2(7) NULL, - create_by nvarchar(64) NULL, + create_by nvarchar(64) DEFAULT('') NULL, update_time datetime2(7) NULL, - update_by nvarchar(64) NULL, + update_by nvarchar(64) DEFAULT('') NULL, del_flag nchar(1) DEFAULT('0') NULL, tenant_id nvarchar(40) NULL, CONSTRAINT PK__flow_tas__3213E83F5AE1F1BA PRIMARY KEY CLUSTERED (id) @@ -709,7 +749,7 @@ EXEC sp_addextendedproperty GO EXEC sp_addextendedproperty -'MS_Description', N'流程状态(0待提交 1审批中 2审批通过 4终止 5作废 6撤销 8已完成 9已退回 10失效 11拿回)', +'MS_Description', N'任务状态(pending待审批 pass通过 auto_pass自动审批 back退回 invalid失效 take_back拿回 copy抄送;任务结果,勿套用实例状态编码)', 'SCHEMA', N'dbo', 'TABLE', N'flow_task', 'COLUMN', N'flow_status' @@ -782,13 +822,13 @@ CREATE TABLE flow_his_task ( definition_id bigint NOT NULL, instance_id bigint NOT NULL, task_id bigint NOT NULL, - node_code nvarchar(200) NULL, - node_name nvarchar(200) NULL, + node_code nvarchar(100) NULL, + node_name nvarchar(100) NULL, node_type tinyint NULL, - target_node_code nvarchar(100) NULL, - target_node_name nvarchar(100) NULL, + target_node_code nvarchar(200) NULL, + target_node_name nvarchar(200) NULL, approver nvarchar(40) NULL, - cooperate_type tinyint DEFAULT('0') NULL, + cooperate_type varchar(20) DEFAULT('none') NOT NULL, collaborator nvarchar(500) NULL, skip_type nvarchar(10) NOT NULL, flow_status nvarchar(20) NOT NULL, @@ -797,6 +837,7 @@ CREATE TABLE flow_his_task ( message nvarchar(500) NULL, variable nvarchar(max) NULL, ext nvarchar(max) NULL, + node_config nvarchar(max) NULL, create_time datetime2(7) NULL, update_time datetime2(7) NULL, del_flag nchar(1) DEFAULT('0') NULL, @@ -879,7 +920,7 @@ EXEC sp_addextendedproperty GO EXEC sp_addextendedproperty -'MS_Description', N'协作方式(1审批 2转办 3委派 4会签 5票签 6加签 7减签)', +'MS_Description', N'协作方式(none无 transfer转办 delegate委派 add_signer加签 remove_signer减签;行为维度,抄送/会签/票签不落此列,结果见 flow_status)', 'SCHEMA', N'dbo', 'TABLE', N'flow_his_task', 'COLUMN', N'cooperate_type' @@ -900,7 +941,7 @@ EXEC sp_addextendedproperty GO EXEC sp_addextendedproperty -'MS_Description', N'流程状态(0待提交 1审批中 2审批通过 4终止 5作废 6撤销 8已完成 9已退回 10失效 11拿回)', +'MS_Description', N'任务状态(pending待审批 pass通过 auto_pass自动审批 back退回 invalid失效 take_back拿回 copy抄送;任务结果,勿套用实例状态编码,唯一例外是撤回归档写 cancel)', 'SCHEMA', N'dbo', 'TABLE', N'flow_his_task', 'COLUMN', N'flow_status' @@ -941,6 +982,13 @@ EXEC sp_addextendedproperty 'COLUMN', N'ext' GO +EXEC sp_addextendedproperty +'MS_Description', N'节点配置(服务任务等引擎配置与自定义扩展属性json)', +'SCHEMA', N'dbo', +'TABLE', N'flow_his_task', +'COLUMN', N'node_config' +GO + EXEC sp_addextendedproperty 'MS_Description', N'任务开始时间', 'SCHEMA', N'dbo', @@ -981,9 +1029,9 @@ CREATE TABLE flow_user ( processed_by nvarchar(80) NULL, associated bigint NOT NULL, create_time datetime2(7) NULL, - create_by nvarchar(64) NULL, + create_by nvarchar(80) NULL, update_time datetime2(7) NULL, - update_by nvarchar(64) NULL, + update_by nvarchar(64) DEFAULT('') NULL, del_flag nchar(1) DEFAULT('0') NULL, tenant_id nvarchar(40) NULL, CONSTRAINT PK__flow_use__3213E83FFA38CA8B PRIMARY KEY CLUSTERED (id) @@ -1006,7 +1054,7 @@ EXEC sp_addextendedproperty GO EXEC sp_addextendedproperty -'MS_Description', N'人员类型(1待办任务的审批人权限 2待办任务的转办人权限 3待办任务的委托人权限)', +'MS_Description', N'人员类型(1待办任务的审批人权限 2待办任务的转办人权限 3待办任务的委托人权限 4待办任务的抄送人)', 'SCHEMA', N'dbo', 'TABLE', N'flow_user', 'COLUMN', N'type' @@ -1020,7 +1068,7 @@ EXEC sp_addextendedproperty GO EXEC sp_addextendedproperty -'MS_Description', N'任务表id', +'MS_Description', N'关联id(待办任务id;抄送人记录为 flow_his_task.id)', 'SCHEMA', N'dbo', 'TABLE', N'flow_user', 'COLUMN', N'associated' @@ -1074,6 +1122,244 @@ EXEC sp_addextendedproperty 'TABLE', N'flow_user' GO +-- 事件等待表:定时 / 消息等需要“等待”的事件统一落在此表,状态独立于流程状态。 +CREATE TABLE flow_event_wait ( + id bigint NOT NULL, + tenant_id nvarchar(40) NULL, + del_flag nchar(1) DEFAULT('0') NULL, + definition_id bigint NULL, + instance_id bigint NULL, + node_code nvarchar(100) NULL, + event_type nvarchar(40) NOT NULL, + event_name nvarchar(100) NULL, + attached_task_id bigint NULL, + trigger_type nvarchar(40) NULL, + trigger_interval nvarchar(100) NULL, + next_trigger_at datetime2(7) NULL, + timezone nvarchar(40) NULL, + repeat_count int NULL, + max_repeat_count int NULL, + status nvarchar(20) NOT NULL, + retry_count int NULL, + max_retry_count int NULL, + retry_at datetime2(7) NULL, + lock_owner nvarchar(64) NULL, + lock_until datetime2(7) NULL, + version int NULL, + payload nvarchar(max) NULL, + last_error nvarchar(500) NULL, + create_time datetime2(7) NULL, + update_time datetime2(7) NULL, + CONSTRAINT PK_flow_event_wait PRIMARY KEY CLUSTERED (id) + WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) + ON [PRIMARY] +) +ON [PRIMARY] +GO + +CREATE NONCLUSTERED INDEX event_wait_status_next ON flow_event_wait (status ASC, next_trigger_at ASC, id ASC) +GO +CREATE NONCLUSTERED INDEX event_wait_status_retry ON flow_event_wait (status ASC, retry_at ASC, id ASC) +GO +CREATE NONCLUSTERED INDEX event_wait_tenant_status_next ON flow_event_wait (tenant_id ASC, status ASC, next_trigger_at ASC, id ASC) +GO +CREATE NONCLUSTERED INDEX event_wait_tenant_status_retry ON flow_event_wait (tenant_id ASC, status ASC, retry_at ASC, id ASC) +GO +CREATE NONCLUSTERED INDEX event_wait_instance_task ON flow_event_wait (instance_id ASC, attached_task_id ASC) +GO +CREATE NONCLUSTERED INDEX event_wait_name_instance_status ON flow_event_wait (event_name ASC, instance_id ASC, status ASC) +GO +CREATE NONCLUSTERED INDEX event_wait_def_node_status ON flow_event_wait (definition_id ASC, node_code ASC, status ASC) +GO + +EXEC sp_addextendedproperty +'MS_Description', N'事件等待表', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'主键id', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'id' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'租户id', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'tenant_id' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'删除标志', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'del_flag' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'流程定义id(定时启动事件必须有)', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'definition_id' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'流程实例id(实例等待事件必须有)', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'instance_id' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'事件节点编码(定位沿用 definition_id + node_code)', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'node_code' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'事件类型(MESSAGE_START TIMER_START TIMER_CATCH MESSAGE_CATCH MESSAGE_THROW TIMER_BOUNDARY MESSAGE_BOUNDARY ERROR_CATCH ERROR_BOUNDARY TERMINATE_END MESSAGE_END)', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'event_type' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'事件名称(消息事件使用)', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'event_name' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'边界事件关联的待办任务id', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'attached_task_id' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'触发策略类型(AFTER FIXED POINT_IN_TIME)', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'trigger_type' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'触发策略原始配置', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'trigger_interval' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'下次触发时间', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'next_trigger_at' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'定时触发使用的时区', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'timezone' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'已触发次数', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'repeat_count' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'最大触发次数', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'max_repeat_count' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'等待状态(WAITING RUNNING CANCELLED RETRY DEAD)', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'status' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'已重试次数', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'retry_count' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'最大重试次数', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'max_retry_count' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'下次重试时间', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'retry_at' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'抢占持有者标识', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'lock_owner' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'抢占租约到期时间', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'lock_until' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'乐观锁版本号', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'version' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'消息事件接收的业务载荷', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'payload' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'最近一次失败原因摘要', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'last_error' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'创建时间', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'create_time' +GO + +EXEC sp_addextendedproperty +'MS_Description', N'更新时间', +'SCHEMA', N'dbo', +'TABLE', N'flow_event_wait', +'COLUMN', N'update_time' +GO + CREATE TABLE flow_category ( category_id bigint NOT NULL, parent_id bigint DEFAULT(0) NULL, @@ -1627,6 +1913,14 @@ GO INSERT sys_menu VALUES (1761400000000011806, N'流程spel表达式定义导出', 1761400000000011801, 5, N'#', N'', NULL, N'N', N'Y', N'F', N'0', N'0', N'workflow:spel:export', N'#', N'', N'', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N''); GO +-- 定时任务(事件等待记录)菜单与按钮 +INSERT sys_menu VALUES (1761400000000011901, N'定时任务', 1761400000000011630, 3, N'timer', N'workflow/timer/index', N'', N'N', N'N', N'C', N'0', N'0', N'workflow:eventWait:list', N'time', N'', N'', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'定时任务与事件等待监控菜单'); +GO +INSERT sys_menu VALUES (1761400000000011902, N'定时任务查询', 1761400000000011901, 1, N'#', N'', NULL, N'N', N'Y', N'F', N'0', N'0', N'workflow:eventWait:query', N'#', N'', N'', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N''); +GO +INSERT sys_menu VALUES (1761400000000011903, N'定时任务手动补发', 1761400000000011901, 2, N'#', N'', NULL, N'N', N'Y', N'F', N'0', N'0', N'workflow:eventWait:trigger', N'#', N'', N'', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N''); +GO + -- 请假测试相关按钮 INSERT sys_menu VALUES (1761400000000011638, N'请假申请', 1761400000000000005, 1, N'leave', N'workflow/leave/index', N'', N'N', N'Y', N'C', N'0', N'0', N'workflow:leave:list', N'#', N'', N'', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'请假申请菜单'); GO @@ -1661,31 +1955,29 @@ INSERT sys_dict_data VALUES (1761600000000000044, 6, N'已退回', N'back', N'wf GO INSERT sys_dict_data VALUES (1761600000000000045, 7, N'已终止', N'termination', N'wf_business_status', N'', N'danger', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'已终止'); GO +INSERT sys_dict_data VALUES (1761600000000000063, 8, N'重启', N'restart', N'wf_business_status', N'', N'info', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'重启'); +GO INSERT sys_dict_data VALUES (1761600000000000046, 1, N'自定义表单', N'static', N'wf_form_type', N'', N'success', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'自定义表单'); GO INSERT sys_dict_data VALUES (1761600000000000047, 2, N'动态表单', N'dynamic', N'wf_form_type', N'', N'primary', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'动态表单'); GO -INSERT sys_dict_data VALUES (1761600000000000048, 1, N'撤销', N'cancel', N'wf_task_status', N'', N'danger', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'撤销'); +INSERT sys_dict_data VALUES (1761600000000000060, 1, N'待审批', N'pending', N'wf_task_status', N'', N'primary', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'待审批'); GO INSERT sys_dict_data VALUES (1761600000000000049, 2, N'通过', N'pass', N'wf_task_status', N'', N'success', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'通过'); GO -INSERT sys_dict_data VALUES (1761600000000000050, 3, N'待审核', N'waiting', N'wf_task_status', N'', N'primary', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'待审核'); +INSERT sys_dict_data VALUES (1761600000000000061, 3, N'自动审批', N'auto_pass', N'wf_task_status', N'', N'success', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'自动审批'); GO -INSERT sys_dict_data VALUES (1761600000000000051, 4, N'作废', N'invalid', N'wf_task_status', N'', N'danger', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'作废'); +INSERT sys_dict_data VALUES (1761600000000000056, 4, N'抄送', N'copy', N'wf_task_status', N'', N'primary', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'抄送'); GO INSERT sys_dict_data VALUES (1761600000000000052, 5, N'退回', N'back', N'wf_task_status', N'', N'danger', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'退回'); GO -INSERT sys_dict_data VALUES (1761600000000000053, 6, N'终止', N'termination', N'wf_task_status', N'', N'danger', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'终止'); +INSERT sys_dict_data VALUES (1761600000000000062, 6, N'拿回', N'take_back', N'wf_task_status', N'', N'warning', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'拿回'); GO -INSERT sys_dict_data VALUES (1761600000000000054, 7, N'转办', N'transfer', N'wf_task_status', N'', N'primary', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'转办'); +INSERT sys_dict_data VALUES (1761600000000000051, 7, N'失效', N'invalid', N'wf_task_status', N'', N'danger', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'失效'); GO -INSERT sys_dict_data VALUES (1761600000000000055, 8, N'委托', N'depute', N'wf_task_status', N'', N'primary', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'委托'); -GO -INSERT sys_dict_data VALUES (1761600000000000056, 9, N'抄送', N'copy', N'wf_task_status', N'', N'primary', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'抄送'); -GO -INSERT sys_dict_data VALUES (1761600000000000057, 10, N'加签', N'sign', N'wf_task_status', N'', N'primary', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'加签'); -GO -INSERT sys_dict_data VALUES (1761600000000000058, 11, N'减签', N'sign_off', N'wf_task_status', N'', N'danger', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'减签'); -GO -INSERT sys_dict_data VALUES (1761600000000000059, 11, N'超时', N'timeout', N'wf_task_status', N'', N'danger', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'超时'); +INSERT sys_dict_data VALUES (1761600000000000048, 8, N'撤销', N'cancel', N'wf_task_status', N'', N'danger', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'撤销'); +INSERT sys_dict_data VALUES (1761600000000000064, 9, N'转办', N'transfer', N'wf_task_status', N'', N'warning', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'转办'); +INSERT sys_dict_data VALUES (1761600000000000065, 10, N'委派', N'delegate', N'wf_task_status', N'', N'warning', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'委派'); +INSERT sys_dict_data VALUES (1761600000000000066, 11, N'加签', N'add_signer', N'wf_task_status', N'', N'warning', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'加签'); +INSERT sys_dict_data VALUES (1761600000000000067, 12, N'减签', N'remove_signer', N'wf_task_status', N'', N'warning', N'N', 1761000000000000103, 1761100000000000001, GETDATE(), NULL, NULL, N'减签'); GO diff --git a/script/sql/update/update_6.0.0-6.1.0.sql b/script/sql/update/update_6.0.0-6.1.0.sql new file mode 100644 index 000000000..e50109a29 --- /dev/null +++ b/script/sql/update/update_6.0.0-6.1.0.sql @@ -0,0 +1,145 @@ +-- ============================================================ +-- RuoYi-Vue-Plus 6.0.0 → 6.1.0 工作流升级脚本(MySQL) +-- 对齐 warm-flow 2.1.0:新增节点/定义配置与事件列、迁移状态编码及事件等待表 +-- 只给存量库执行;全新建库直接用 script/sql/ry_workflow.sql,无需本脚本 +-- 请在启动已开启 warm-flow.timer-enabled 的 2.1.0 应用之前,按顺序执行本脚本 +-- 执行完到「系统管理 → 缓存管理 → 刷新缓存」刷一次字典缓存,否则前端仍显示旧文案 +-- ============================================================ + +-- 1. 节点版本已统一由流程定义管理,flow_node.version 在引擎 1.8.9 起就不存在了 +-- 旧全量脚本里该列是 NOT NULL 且无默认值,引擎写节点会报「doesn't have a default value」 +ALTER TABLE `flow_node` DROP COLUMN `version`; + +-- 2. 补齐 2.1.0 的定义、服务任务、抄送与事件节点配置列 +ALTER TABLE `flow_definition` + ADD COLUMN `def_config` text NULL COMMENT '流程定义配置(自动审批等引擎配置与自定义扩展属性json)' AFTER `ext`; + +ALTER TABLE `flow_node` + ADD COLUMN `node_config` text NULL COMMENT '节点配置(服务任务、事件等引擎节点配置json)' AFTER `ext`, + ADD COLUMN `event_type` varchar(40) NULL COMMENT '事件类型' AFTER `node_config`, + ADD COLUMN `event_name` varchar(100) NULL COMMENT '事件名称(消息事件使用)' AFTER `event_type`, + ADD COLUMN `attached_to_node_code` varchar(100) NULL COMMENT '边界事件附着的任务节点编码' AFTER `event_name`, + ADD COLUMN `interrupting` tinyint(1) NULL COMMENT '边界事件是否中断型' AFTER `attached_to_node_code`; + +ALTER TABLE `flow_his_task` + ADD COLUMN `node_config` text NULL COMMENT '节点配置(服务任务、事件等引擎节点配置json)' AFTER `ext`; + +-- 3. 实例、任务和协作状态改用独立的字符串编码 +ALTER TABLE `flow_instance` + MODIFY COLUMN `flow_status` varchar(20) NOT NULL COMMENT '实例状态(draft waiting termination invalid cancel finish back restart)'; +ALTER TABLE `flow_task` + MODIFY COLUMN `flow_status` varchar(20) NOT NULL COMMENT '任务状态(pending pass auto_pass back invalid take_back copy)'; +ALTER TABLE `flow_his_task` + MODIFY COLUMN `flow_status` varchar(20) NOT NULL COMMENT '任务状态(pending pass auto_pass back invalid take_back copy;撤回归档为 cancel)', + MODIFY COLUMN `cooperate_type` varchar(20) NOT NULL DEFAULT 'none' COMMENT '协作方式(none transfer delegate add_signer remove_signer)'; +ALTER TABLE `flow_user` + MODIFY COLUMN `type` char(1) NOT NULL COMMENT '人员类型(1审批人 2转办人 3委托人 4抄送人)', + MODIFY COLUMN `associated` bigint NOT NULL COMMENT '关联id(待办任务id;抄送人记录为 flow_his_task.id)'; + +-- 旧数字码只在命中时转换,已经使用新编码的数据不会改变 +UPDATE flow_instance SET flow_status = CASE flow_status + WHEN '0' THEN 'draft' WHEN '1' THEN 'waiting' WHEN '4' THEN 'termination' + WHEN '5' THEN 'invalid' WHEN '6' THEN 'cancel' WHEN '7' THEN 'waiting' + WHEN '8' THEN 'finish' WHEN '9' THEN 'back' WHEN '12' THEN 'restart' + WHEN '13' THEN 'draft' END +WHERE flow_status IN ('0', '1', '4', '5', '6', '7', '8', '9', '12', '13'); + +UPDATE flow_task SET flow_status = CASE flow_status + WHEN '1' THEN 'pending' WHEN '2' THEN 'pass' WHEN '8' THEN 'pass' + WHEN '9' THEN 'back' WHEN '10' THEN 'invalid' WHEN '11' THEN 'take_back' END +WHERE flow_status IN ('1', '2', '8', '9', '10', '11'); + +UPDATE flow_his_task SET flow_status = CASE flow_status + WHEN '1' THEN 'pending' WHEN '2' THEN 'pass' WHEN '8' THEN 'pass' + WHEN '9' THEN 'back' WHEN '10' THEN 'invalid' WHEN '11' THEN 'take_back' END +WHERE flow_status IN ('1', '2', '8', '9', '10', '11'); + +UPDATE flow_his_task SET cooperate_type = CASE cooperate_type + WHEN '0' THEN 'none' WHEN '1' THEN 'none' WHEN '2' THEN 'transfer' + WHEN '3' THEN 'delegate' WHEN '4' THEN 'none' WHEN '5' THEN 'none' + WHEN '6' THEN 'add_signer' WHEN '7' THEN 'remove_signer' END +WHERE cooperate_type IN ('0', '1', '2', '3', '4', '5', '6', '7'); + +-- 4. 任务状态字典改为引擎 TaskStatus 的 7 个码,外加撤回归档留痕的 cancel +DELETE FROM sys_dict_data WHERE dict_type = 'wf_task_status'; +INSERT INTO sys_dict_data VALUES (1761600000000000060, 1, '待审批', 'pending', 'wf_task_status', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '待审批'); +INSERT INTO sys_dict_data VALUES (1761600000000000049, 2, '通过', 'pass', 'wf_task_status', '', 'success', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '通过'); +INSERT INTO sys_dict_data VALUES (1761600000000000061, 3, '自动审批', 'auto_pass', 'wf_task_status', '', 'success', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '自动审批'); +INSERT INTO sys_dict_data VALUES (1761600000000000056, 4, '抄送', 'copy', 'wf_task_status', '', 'primary', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '抄送'); +INSERT INTO sys_dict_data VALUES (1761600000000000052, 5, '退回', 'back', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '退回'); +INSERT INTO sys_dict_data VALUES (1761600000000000062, 6, '拿回', 'take_back', 'wf_task_status', '', 'warning', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '拿回'); +INSERT INTO sys_dict_data VALUES (1761600000000000051, 7, '失效', 'invalid', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '失效'); +INSERT INTO sys_dict_data VALUES (1761600000000000048, 8, '撤销', 'cancel', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '撤销'); +INSERT INTO sys_dict_data VALUES (1761600000000000064, 9, '转办', 'transfer', 'wf_task_status', '', 'warning', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '转办'); +INSERT INTO sys_dict_data VALUES (1761600000000000065, 10, '委派', 'delegate', 'wf_task_status', '', 'warning', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '委派'); +INSERT INTO sys_dict_data VALUES (1761600000000000066, 11, '加签', 'add_signer', 'wf_task_status', '', 'warning', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '加签'); +INSERT INTO sys_dict_data VALUES (1761600000000000067, 12, '减签', 'remove_signer', 'wf_task_status', '', 'warning', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '减签'); + +-- 原来的 waiting / termination / transfer / depute / sign / sign_off / timeout 已随上面整组删除: +-- waiting 属实例维度;termination 也是终止归档留痕使用的编码,在下方 2.1.1 适配段补回。 +-- 协作动作使用 transfer / delegate / add_signer / remove_signer,timeout 不属于任务状态。 +-- take_back 目前没有写入方,是留给「拿回」功能的占位,别当冗余删掉。 + +-- 5. 实例状态字典补 restart(引擎 FlowStatus 有,字典缺) +INSERT INTO sys_dict_data VALUES (1761600000000000063, 8, '重启', 'restart', 'wf_business_status', '', 'info', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '重启'); + +-- 6. 事件等待表:定时 / 消息等需要「等待」的事件统一落在此表(与全量脚本一致)。 +-- 6.0.0 存量库没有这张表,不建表则定时器与定时任务页面全部不可用 +-- 注意:建表后还需在 application.yml 打开 warm-flow.timer-enabled: true,引擎才会扫描本表 +CREATE TABLE IF NOT EXISTS `flow_event_wait` +( + `id` bigint NOT NULL COMMENT '主键id', + `tenant_id` varchar(40) DEFAULT NULL COMMENT '租户id', + `del_flag` char(1) DEFAULT '0' COMMENT '删除标志', + `definition_id` bigint DEFAULT NULL COMMENT '流程定义id(定时启动事件必须有)', + `instance_id` bigint DEFAULT NULL COMMENT '流程实例id(实例等待事件必须有)', + `node_code` varchar(100) DEFAULT NULL COMMENT '事件节点编码(定位沿用 definition_id + node_code)', + `event_type` varchar(40) NOT NULL COMMENT '事件类型(MESSAGE_START TIMER_START TIMER_CATCH MESSAGE_CATCH MESSAGE_THROW TIMER_BOUNDARY MESSAGE_BOUNDARY ERROR_CATCH ERROR_BOUNDARY TERMINATE_END MESSAGE_END)', + `event_name` varchar(100) DEFAULT NULL COMMENT '事件名称(消息事件使用)', + `attached_task_id` bigint DEFAULT NULL COMMENT '边界事件关联的待办任务id', + `trigger_type` varchar(40) DEFAULT NULL COMMENT '触发策略类型(AFTER FIXED POINT_IN_TIME)', + `trigger_interval` varchar(100) DEFAULT NULL COMMENT '触发策略原始配置', + `next_trigger_at` datetime DEFAULT NULL COMMENT '下次触发时间', + `timezone` varchar(40) DEFAULT NULL COMMENT '定时触发使用的时区', + `repeat_count` int DEFAULT NULL COMMENT '已触发次数', + `max_repeat_count` int DEFAULT NULL COMMENT '最大触发次数', + `status` varchar(20) NOT NULL COMMENT '等待状态(WAITING RUNNING CANCELLED RETRY DEAD)', + `retry_count` int DEFAULT NULL COMMENT '已重试次数', + `max_retry_count` int DEFAULT NULL COMMENT '最大重试次数', + `retry_at` datetime DEFAULT NULL COMMENT '下次重试时间', + `lock_owner` varchar(64) DEFAULT NULL COMMENT '抢占持有者标识', + `lock_until` datetime DEFAULT NULL COMMENT '抢占租约到期时间', + `version` int DEFAULT NULL COMMENT '乐观锁版本号', + `payload` text COMMENT '消息事件接收的业务载荷', + `last_error` varchar(500) DEFAULT NULL COMMENT '最近一次失败原因摘要', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `event_wait_status_next` (`status`, `next_trigger_at`, `id`) USING BTREE, + KEY `event_wait_status_retry` (`status`, `retry_at`, `id`) USING BTREE, + KEY `event_wait_tenant_status_next` (`tenant_id`, `status`, `next_trigger_at`, `id`) USING BTREE, + KEY `event_wait_tenant_status_retry` (`tenant_id`, `status`, `retry_at`, `id`) USING BTREE, + KEY `event_wait_instance_task` (`instance_id`, `attached_task_id`) USING BTREE, + KEY `event_wait_name_instance_status` (`event_name`, `instance_id`, `status`) USING BTREE, + KEY `event_wait_def_node_status` (`definition_id`, `node_code`, `status`) USING BTREE +) ENGINE = InnoDB COMMENT ='事件等待表'; + +-- 7. 「流程监控」下补「定时任务」菜单 + 查询/手动补发按钮(与 script/sql/ry_workflow.sql 全量脚本一致) +-- 先删后插保证可重复执行;重新执行后记得给角色重新勾选按钮权限 +DELETE FROM sys_menu WHERE menu_id IN (1761400000000011901, 1761400000000011902, 1761400000000011903); +insert into sys_menu values (1761400000000011901, '定时任务', 1761400000000011630, 3, 'timer', 'workflow/timer/index', '', 'N', 'N', 'C', '0', '0', 'workflow:eventWait:list', 'time', '', '', 1761000000000000103, 1761100000000000001, sysdate(), null, null, '定时任务与事件等待监控菜单'); +insert into sys_menu values (1761400000000011902, '定时任务查询', 1761400000000011901, 1, '#', '', '', 'N', 'Y', 'F', '0', '0', 'workflow:eventWait:query', '#', '', '', 1761000000000000103, 1761100000000000001, sysdate(), null, null, ''); +insert into sys_menu values (1761400000000011903, '定时任务手动补发', 1761400000000011901, 2, '#', '', '', 'N', 'Y', 'F', '0', '0', 'workflow:eventWait:trigger', '#', '', '', 1761000000000000103, 1761100000000000001, sysdate(), null, null, ''); + +-- 8. Warm-Flow 2.1.1 适配(无引擎表结构变更) +-- 已执行过本脚本 2.1.0 迁移的数据库只需执行本段,无需重跑前面的迁移。 +-- 补充任务终止归档字典,执行后刷新字典缓存;恢复接口复用页面权限 workflow:eventWait:list。 +INSERT INTO sys_dict_data +SELECT 1761600000000000068, 13, '已终止', 'termination', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '终止归档留痕' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'wf_task_status' AND dict_value = 'termination'); + +-- 存量数据检查(仅查询,不自动改写审批历史):2.1.0 可能把实例 waiting 写入待办状态。 +SELECT id, instance_id, node_code, flow_status FROM flow_task WHERE flow_status = 'waiting'; +-- 确认上述记录为待审批任务后,可在备份并暂停流程写入的维护窗口执行: +-- UPDATE flow_task SET flow_status = 'pending' WHERE flow_status = 'waiting'; +-- 旧撤回历史 cancel 保留原值及字典,新撤回历史由引擎写 back;终止归档仍写 termination。