Compare commits

..

51 Commits

Author SHA1 Message Date
疯狂的狮子Li 7180b52977 🦁🦁🦁发布 6.0.0 正式版 大版本发布!!! 2026-07-30 11:02:02 +08:00
疯狂的狮子Li 070d0bdd45 update snail-ai 1.1.1 2026-07-30 09:25:07 +08:00
疯狂的狮子Li a2e3eda278 update 优化 开放ai系统智能体市场 2026-07-29 10:50:50 +08:00
疯狂的狮子Li ac61c0a558 update snail-ai 1.1.0 2026-07-27 11:46:02 +08:00
疯狂的狮子Li a562c78d27 update snail-job 2.0.2 2026-07-24 09:34:14 +08:00
疯狂的狮子Li 071db648cd update 优化 兼容企业微信 大小写返回不一致的奇葩问题 2026-07-22 19:10:43 +08:00
疯狂的狮子Li 47a71acf49 update 优化 使用三方修改的justauth版本 去掉fastjson 2026-07-22 11:39:57 +08:00
疯狂的狮子Li 46cef1c6c7 update 优化 增加工作流内部异常拦截 返回框架标准信息 2026-07-20 18:03:27 +08:00
疯狂的狮子Li d6760f93d5 update 优化 删除流程实例同时删除抄送内容 2026-07-20 11:49:24 +08:00
疯狂的狮子Li da5f30cae2 update 升级 全部依赖版本与适配性代码修改 2026-07-16 18:39:40 +08:00
疯狂的狮子Li 28c39d5b90 update 优化 自动装配警告问题 2026-07-15 11:30:01 +08:00
疯狂的狮子Li d3146a5755 update 优化 bo去掉BaseEntity之后 不生成基础字段问题 2026-07-15 09:44:03 +08:00
AprilWind e8f2e68f0e !871 update 优化数据库字段类型处理
* update 扩展数据库字段类型支持,增加浮点型和其他类型的规范化处理
2026-07-08 06:55:18 +00:00
疯狂的狮子Li c2667cfbe1 fix 修复 liteflow 公共节点包扫描问题 2026-07-08 12:08:36 +08:00
疯狂的狮子Li 44a9bd1006 update 优化 liteflow 整体上下文用法优化 2026-07-07 15:48:55 +08:00
AprilWind 543037885d !869 update 移除冗余注释和未使用的变量
* update 移除冗余注释和未使用的变量
2026-07-07 06:46:54 +00:00
疯狂的狮子Li d7eca3db85 update 优化 适配多前端 skill 内容 2026-07-06 16:15:41 +08:00
tangyh 348141427d !868 fix: 修复菜单构建方法返回空列表的情况,确保在无菜单时返回空集合
* fix: 修复菜单构建方法返回空列表的情况,确保在无菜单时返回空集合
2026-07-06 01:24:29 +00:00
疯狂的狮子Li 4baa9edfa1 update 优化 token过期提醒 2026-07-06 09:23:23 +08:00
疯狂的狮子Li a5b65da892 fix 修复 请求流程删除失败问题 2026-07-03 18:30:40 +08:00
疯狂的狮子Li 126380e9f5 fix 修复 用户上传头像后无法编辑问题 头像url改为独立字段避免影响 2026-07-03 17:00:38 +08:00
疯狂的狮子Li 23387d71d8 fix 修复 warmflow 时间用的还是Date类 导致无法注入数据问题 2026-07-03 16:24:25 +08:00
疯狂的狮子Li 59f1e75bdc fix 修复 删除sqlserver内多余的租户id 2026-07-03 11:02:49 +08:00
疯狂的狮子Li c4b1e58c3e update 优化 gen模块 默认激活 避免很多人不知道这个配置 2026-07-02 14:41:27 +08:00
疯狂的狮子Li d9894b534d update: 基于 LiteFlow 重构工作流业务编排 2026-07-02 14:39:50 +08:00
疯狂的狮子Li c8f755cbde add: 新增工作流 LiteFlow 规则链 2026-07-02 14:39:10 +08:00
疯狂的狮子Li 53f857714d add: 新增 ruoyi-common-liteflow 规则引擎模块 2026-07-02 14:37:53 +08:00
Nottyjay 6876bd355f !867 fix 修复 非常规字段生成时htmlType为null导致模板生成失败
* fix 修复 非常规字段生成时htmlType为null导致模板生成失败
2026-07-01 03:02:15 +00:00
疯狂的狮子Li 7ca18b7798 update 重写 s3 授权签名 支持私有库自定义域名 2026-07-01 09:44:15 +08:00
Kayleigh Wang dbbc790081 !865 fix 修正代码生成模块文档扫描包名 org.dromara.generator -> org.dromara.gen
* fix 修正代码生成模块文档扫描包名 org.dromara.generator -> org.dromara.gen
2026-07-01 01:10:04 +00:00
疯狂的狮子Li 4e6dbee008 fix 修复 响应加密编码错误问题 2026-06-30 16:50:59 +08:00
疯狂的狮子Li e571f339b4 update 更新 react 模板适配 2026-06-30 11:36:07 +08:00
YueYe 74ed2734f0 !864 添加lock4j的默认失败策略
* 添加lock4j的默认失败策略
2026-06-30 03:00:03 +00:00
疯狂的狮子Li 1fbe1694e4 fix 修复 检查bean是否存在注解不生效问题 2026-06-29 18:14:35 +08:00
疯狂的狮子Li c56dc6b8e9 update 优化 如果是react前端生成代码 目录使用pages命名 2026-06-29 16:42:40 +08:00
疯狂的狮子Li 1be4f7b07e update snail-ai 升级 1.0.0 正式版 2026-06-29 13:47:57 +08:00
疯狂的狮子Li 5392c3228e fix 修复 单独关闭 openapi client 报错问题 2026-06-26 10:52:12 +08:00
疯狂的狮子Li 837604f889 update 优化 oss 协议头拼接写法 2026-06-26 10:20:15 +08:00
疯狂的狮子Li eab4228548 fix 修复 push 模块 sse/ws 浏览器多标签页互挤掉线问题 2026-06-25 17:23:44 +08:00
疯狂的狮子Li e8b033c4ef fix 修复 代码生成 模板方法调用写法错误 2026-06-25 14:56:33 +08:00
疯狂的狮子Li 3cf2eceb13 fix 修复 fm模板将Long类型格式化出现问题 2026-06-25 09:30:11 +08:00
疯狂的狮子Li 1d263f7656 update 优化 处理一些代码生成模板的一些写法问题 2026-06-24 17:33:04 +08:00
专业坑队 c1e344be0e !863 fix(generator): 优化代码生成器模板和逻辑
* fix(generator): 优化代码生成器模板和逻辑
2026-06-24 09:15:22 +00:00
疯狂的狮子Li 612a965c26 update spring-boot-admin 4.1.1 2026-06-23 13:03:38 +08:00
疯狂的狮子Li d7c525d7f8 update 优化 异常处理 2026-06-23 11:39:58 +08:00
疯狂的狮子Li 93a07e57b8 update 优化 基于流程变量优化退回选择问题 2026-06-22 17:04:14 +08:00
疯狂的狮子Li 642c713f78 update 优化 工作流管理页接口增加权限标识符 避免越权 2026-06-22 17:03:48 +08:00
疯狂的狮子Li 123ba9bfba fix 修复 demo数据导致报错问题 2026-06-22 14:44:02 +08:00
疯狂的狮子Li 66b4840356 update 优化 删除无用配置代码 2026-06-22 14:43:49 +08:00
疯狂的狮子Li 14e2cf16cc fix 修复 springdoc 自定义配置在分组下不生效问题 接口切换为全局接口 全局生效 2026-06-22 11:55:26 +08:00
疯狂的狮子Li f3256442c8 fix 修复 白名单路径无法存储多个 2026-06-22 11:43:38 +08:00
111 changed files with 2388 additions and 816 deletions
+1 -1
View File
@@ -61,4 +61,4 @@ description: 标准后端 CRUD 专家。用于当前项目中的新增单表 CRU
- BO / VO / Entity 是否职责分离
- 导出、分页、删除前校验是否齐全
- 是否只是 generator 裸产物,如果是要继续补齐项目约定
- 前端 `api/types/index.vue` 如需同步,接口路径、返回结构、日期范围参数要与后端一致
- 前端 `api/types` 和 Vue `index.vue` 或 React `index.tsx` 如需同步,接口路径、返回结构、日期范围参数要与后端一致
+2 -2
View File
@@ -12,12 +12,12 @@ description: 后端工程总入口。用于在当前 RuoYi-Vue-Plus 项目中识
3. 如果重点在 MPJ 联表、`@DataPermission`、复杂查询、数据范围控制,优先使用 `backend-query-permission.md` 的规则。
4. 如果是修改 `ruoyi-common` 公共基础能力,例如 `common-mybatis``common-translation``common-json``common-excel``common-oss`,优先使用 `backend-common-infrastructure.md` 的规则。
5. 如果只要求补充或修正 JavaDoc 注释,优先使用 `backend-javadoc.md` 的规则。
6. 如果同时要求同步前端接口或前端页面骨架,保持后端路由与 generator 风格稳定,便于前端 agent 对接。
6. 如果同时要求同步前端接口或前端页面骨架,先确认目标前端是 Vue 还是 React,保持后端路由与 generator 风格稳定,便于前端 agent 对接。
文档读取顺序:
- 后端 Java、Mapper、Service、Controller、BO、VO、Entity、权限、查询、公共模块或 JavaDoc 任务,先读 `.codex/skills/ruoyi-plus-ai-coding/references/backend.md`
- 同步前端 Vue、TypeScript、api、types 或页面骨架时,再读 `.codex/skills/ruoyi-plus-ai-coding/references/frontend.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 模板和真实代码冲突,优先相信当前模块真实代码和实际调用点。
+5 -5
View File
@@ -1,6 +1,6 @@
---
name: ruoyi-plus-ai-coding
description: 在仓库内按代码生成器模板、项目 reference 文档和既有约定生成或修改代码。用于新增或修改 CRUD 模块、controller/service/mapper/BO/VO/entity、MyBatis-Plus/MPJ 查询、数据权限、缓存、翻译/JSON 增强、公共 common 模块能力、JavaDoc 注释,以及与后端接口配套的 Vue 3 + TypeScript 页面、types 和 api 文件;触发后应先按任务类型读取对应 references,再阅读目标模块真实代码和 generator 模板。
description: 在仓库内按代码生成器模板、项目 reference 文档和既有约定生成或修改代码。用于新增或修改 CRUD 模块、controller/service/mapper/BO/VO/entity、MyBatis-Plus/MPJ 查询、数据权限、缓存、翻译/JSON 增强、公共 common 模块能力、JavaDoc 注释,以及与后端接口配套的 Vue 或 React 前端页面、types 和 api 文件;触发后应先按任务类型读取对应 references,再阅读目标模块真实代码和 generator 模板。
---
# RuoYi Plus AI 编码规范
@@ -17,7 +17,7 @@ description: 在仓库内按代码生成器模板、项目 reference 文档和
- 修改 `ruoyi-common` 公共能力,例如 mybatis 查询构造器、translation、json enhance、excel、oss、redis、web 配置。
- 补充或修正 JavaDoc 注释,尤其是公共 API、接口、BO/VO/Entity 字段、Mapper 默认方法、Service/Controller 方法。
- 在系统、监控、工作流、demo 等模块内按现有约定扩展业务代码。
- 为后端新增接口同步补前端 `api/types/index.vue` 骨架。
- 为后端新增接口同步补前端 `api/types` 和 Vue `index.vue` 或 React `index.tsx` 页面骨架。
## 不适用场景
@@ -45,7 +45,7 @@ description: 在仓库内按代码生成器模板、项目 reference 文档和
使用本 skill 时,先按任务类型读取适用 reference,不一次性展开所有文档:
- 后端 Java、Mapper、Service、Controller、BO、VO、Entity、权限、查询、公共模块或 JavaDoc 任务,先读 [references/backend.md](references/backend.md)。
- 前端 Vue、TypeScript、api、types 或页面任务,先读 [references/frontend.md](references/frontend.md)。
- 前端 Vue、React、TypeScript、api、types 或页面任务,先读 [references/frontend.md](references/frontend.md)。
- 不确定任务边界、需要标准调用方式或需要对照典型场景时,再读 [references/examples.md](references/examples.md)。
reference 用来约束实现方式和自检范围;发生冲突时,仍以当前模块真实代码和实际调用点为准。
@@ -71,7 +71,7 @@ Java、MyBatis-Plus、BO/VO/entity、controller、mapper、service 的具体规
## 前端规则
Vue 3、TypeScript API 文件、生成式列表页、表单状态、字典和日期范围约定见 [references/frontend.md](references/frontend.md)。
Vue 3、React、TypeScript API 文件、生成式列表页、表单状态、字典和日期范围约定见 [references/frontend.md](references/frontend.md)。
## 使用案例
@@ -163,7 +163,7 @@ Vue 3、TypeScript API 文件、生成式列表页、表单状态、字典和日
- BO 使用 `@AutoMapper(target = Entity.class, reverseConvertGenerate = false)`
- VO 使用 `@AutoMapper(target = Entity.class)`
- 前端 API 路径与后端路由完全对应。
- 前端列表页继续使用仓库里的 `proxy?.addDateRange``proxy?.$modal``proxy?.download``useDict``pagination` 等工具
- 前端列表页继续使用对应前端工程已有工具:Vue 侧如 `proxy?.addDateRange``proxy?.$modal``proxy?.download``useDict``pagination`React 侧如 `ProTable``ModalForm``useTableSelection``useDateRangeQuery``useTableExport`
## 推荐提问方式
@@ -62,13 +62,13 @@
### 用户提问示例
```text
使用 $ruoyi-plus-ai-coding 为 monitor/cache 新增一个导出接口,并同步补齐前端 api/types 调用骨架。
使用 $ruoyi-plus-ai-coding 为 monitor/cache 新增一个导出接口,并同步补齐 Vue 或 React 前端 api/types 调用骨架。
```
### 期望执行方式
- 先补后端 `controller/service`
- 再根据后端路由补前端 `src/api` 或 generator 风格的前端骨架。
- 再根据后端路由和目标前端类型`src/api` 或 generator 风格的前端骨架。
- 保证导出接口路径和前端下载调用一致。
## 案例 6:推荐的高质量任务描述
@@ -6,7 +6,10 @@
- 默认 Vue 模板在 `fm/vue`React 模板在 `fm/react`
- 前端工程中与目标模块最接近的现有页面
当前 boot4 仓库通常只含后端与 generator 前端模板;如果前端工程不在当前 root,先以 generator 模板约定为准,再对照用户提供的前端目录或相邻仓库。
当前 boot4 仓库通常只含后端与 generator 前端模板;如果前端工程不在当前仓库根目录,先以 generator 模板约定为准,再对照用户提供的前端工程或官方前端分支:
- Vue 前端:`https://gitee.com/JavaLionLi/plus-ui/tree/6.X-Vue`
- React 前端:`https://gitee.com/JavaLionLi/plus-ui/tree/6.X-React`
## 前端模板选择规则
@@ -17,11 +20,10 @@
## API 文件规则
-`@/utils/request` 引入 `request`
-`@/utils/api-types` 引入 `AxiosPromise`
- `@/api/types` 引入 `PageResult`
- `@/api/<module>/<business>/types` 引入本模块类型
- 列表接口通常返回 `AxiosPromise<PageResult<Vo>>`
- Vue 模板`@/utils/request` 引入 `request`,从 `@/utils/api-types` 引入 `AxiosPromise`,从 `@/api/types` 引入 `PageResult`
- React 模板从 `@/api/request` 引入 `request``@/api/types` 引入 `R``PageResult`
- 本模块类型:Vue 模板从 `@/api/<module>/<business>/types` 引入,React 模板从 `./types` 引入
- Vue 列表接口通常返回 `AxiosPromise<PageResult<Vo>>`React 列表接口通常返回 `request<R<PageResult<Vo>>>(...)`
- 常规接口命名和路由保持:
`listXxx` -> `GET /<module>/<business>/list`
`getXxx` -> `GET /<module>/<business>/{id}`
@@ -38,7 +40,7 @@
- Java 数值类型通常映射为 `number`
- Boolean 映射为 `boolean`
- 其他生成字段默认多为 `string`
- 存在日期范围查询时保留 `params?: any`
- 存在日期范围查询时保留 `params`Vue 模板通常是 `params?: any`React 模板通常是 `params?: Record<string, unknown>`
## Vue 页面规则
@@ -51,7 +53,19 @@
- 弹窗状态优先由 `useFormDialog` 返回的 `dialog``openDialog``showDialog``closeDialog` 管理。
- 表单引用通常命名为 `queryFormRef``<business>FormRef`
## 页面行为规则
## React 页面规则
- 使用 `index.tsx`,组件默认导出 `<BusinessName>Page`
- 页面主体优先沿用 Ant Design Pro`PageContainer``ProTable``ModalForm``ProColumns``ActionType`
- 表单优先使用 `Form.useForm<Form>()`,弹窗开关优先使用 `ahooks``useBoolean`
- 权限通过 `useUserStore``userInfo`,再用 `hasPermi(userInfo, ['module:business:action'])` 生成 `canAdd``canEdit``canRemove``canExport`
- 表格选择使用 `useTableSelection<VO>(row => row.id)`;表格刷新使用 `actionRef.current?.reload()``reloadAndRest?.()`
- 字典使用 `useDict``dictOptions`,展示使用 `DictTag`
- 日期范围使用 `useDateRangeQuery`,在 `ProTable``request` 中由 `toPageQuery(params)` 转查询参数后再应用范围字段。
- 导出使用 `useTableExport`,路径保持 `/<module>/<business>/export`
- 文件、图片、富文本组件使用 React 工程已有的 `FileUpload``ImageUpload``ImagePreview``RichTextEditor`
## Vue 页面行为规则
- `getList` 负责通过 `withLoading` 设置 loading、处理日期范围参数、调用列表接口、回填 `rows``total`
- `handleQuery` 通常先把 `pageNum` 重置为 `1`,再重新查询。
@@ -63,13 +77,20 @@
- `handleDelete` 使用 `modal.confirm(...)` 确认,再调用删除接口并刷新。
- `handleExport` 使用 `download as requestDownload``@/utils/request` 导出的下载方法。
## React 页面行为规则
- React `ProTable` 页面通过 `request` 回调加载列表并返回 `toTableData(res)`;新增、修改、删除成功后调用 `actionRef` 刷新。
- React 弹窗提交函数根据主键判断调用新增还是更新,成功后 `message.success('操作成功')` 并重置表单。
## 模板结构规则
- 优先保持生成器的页面布局结构:搜索区卡片、表格区卡片、工具栏、分页、弹窗表单
- 保留 `v-hasPermi="['module:business:add']"` 这类权限指令。
- 继续使用仓库已有组件:`right-toolbar``pagination``dict-tag``image-preview``image-upload``file-upload``editor`
- 优先保持生成器的页面布局结构,不在 Vue 和 React 之间互相移植组件体系
- Vue 保留 `v-hasPermi="['module:business:add']"` 这类权限指令。
- Vue 继续使用仓库已有组件:`right-toolbar``pagination``dict-tag``image-preview``image-upload``file-upload``editor`
- React 继续使用仓库已有组件:`RowActions``DictTag``ImagePreview``ImageUpload``FileUpload``RichTextEditor`
- 已有页面对时间列使用 `parseTime` 时,新页面保持一致。
- BETWEEN 日期查询继续使用 `el-date-picker`,脚本侧通过 `useDateRangeQuery` 生成 `dateRangeXxx``applyXxxDateRange``resetXxxDateRange`
- Vue BETWEEN 日期查询继续使用 `el-date-picker`,脚本侧通过 `useDateRangeQuery` 生成 `dateRangeXxx``applyXxxDateRange``resetXxxDateRange`
- React BETWEEN 日期查询继续使用 `ProTable``dateTimeRange` 搜索列,查询侧通过 `useDateRangeQuery` 写入 `params`
## 避免事项
@@ -77,3 +98,4 @@
- 模块已使用字典时,不要把选项文案硬编码到页面里。
- 不要让 API 函数名和路由段偏离后端约定。
- 后端 BO/service 依赖 begin/end 参数时,不要从查询对象里删掉 `params` 和日期范围处理。
- 不要把 Vue 的 `proxy``v-hasPermi`、Element Plus 组件写进 React 页面,也不要把 React 的 `ProTable``ModalForm`、Ant Design 权限判断写进 Vue 页面。
+24 -16
View File
@@ -14,21 +14,21 @@
<properties>
<!-- 项目基础配置 -->
<revision>6.0.0-BETA</revision>
<revision>6.0.0</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>21</java.version>
<!-- Spring 生态依赖版本 -->
<spring-boot.version>4.1.0</spring-boot.version>
<spring-boot-admin.version>4.1.0</spring-boot-admin.version>
<spring-boot-admin.version>4.1.2</spring-boot-admin.version>
<spring-ai.version>2.0.0</spring-ai.version>
<springdoc.version>3.0.3</springdoc.version>
<!-- 数据访问与持久化相关依赖版本 -->
<mybatis.version>3.5.19</mybatis.version>
<mybatis-plus.version>3.5.16</mybatis-plus.version>
<mybatis-plus-join.version>1.5.7</mybatis-plus-join.version>
<mybatis-plus.version>3.5.17</mybatis-plus.version>
<mybatis-plus-join.version>1.5.9</mybatis-plus-join.version>
<dynamic-ds.version>4.5.0</dynamic-ds.version>
<anyline.version>8.7.3-20260306</anyline.version>
<easy-es.version>3.0.2</easy-es.version>
@@ -36,35 +36,36 @@
<!-- 安全认证与加密相关依赖版本 -->
<satoken.version>1.45.0</satoken.version>
<justauth.version>1.16.7</justauth.version>
<bouncycastle.version>1.84</bouncycastle.version>
<justauth.version>2.0.0</justauth.version>
<bouncycastle.version>1.85</bouncycastle.version>
<!-- 缓存、锁与任务调度相关依赖版本 -->
<redisson.version>4.6.1</redisson.version>
<lock4j.version>2.2.7</lock4j.version>
<snailjob.version>2.0.0</snailjob.version>
<snailai.version>0.0.6</snailai.version>
<snailjob.version>2.0.2</snailjob.version>
<snailai.version>1.1.1</snailai.version>
<!-- 通用工具、编译增强与代码生成相关依赖版本 -->
<hutool.version>5.8.46</hutool.version>
<hutool.version>5.8.47</hutool.version>
<mapstruct-plus.version>1.5.1</mapstruct-plus.version>
<mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
<lombok.version>1.18.42</lombok.version>
<lombok.version>1.18.46</lombok.version>
<therapi-javadoc.version>0.15.0</therapi-javadoc.version>
<fesod.version>2.0.2-incubating</fesod.version>
<fory.version>1.2.0</fory.version>
<fory.version>1.3.0</fory.version>
<!-- 三方集成与业务扩展相关依赖版本 -->
<!-- 离线IP地址定位库 -->
<ip2region.version>3.3.6</ip2region.version>
<ip2region.version>3.3.7</ip2region.version>
<!-- OSS 配置 -->
<aws.sdk.version>2.42.9</aws.sdk.version>
<aws.sdk.version>2.48.1</aws.sdk.version>
<!-- SMS 配置 -->
<sms4j.version>3.3.5</sms4j.version>
<!-- 工作流配置 -->
<warm-flow.version>1.8.8</warm-flow.version>
<warm-flow.version>1.8.9</warm-flow.version>
<liteflow.version>2.16.0</liteflow.version>
<!-- mqtt客户端 -->
<mica-mqtt.version>2.6.6</mica-mqtt.version>
<mica-mqtt.version>2.6.8</mica-mqtt.version>
<!-- Maven 构建插件版本 -->
<maven-jar-plugin.version>3.5.0</maven-jar-plugin.version>
@@ -358,6 +359,13 @@
<version>${warm-flow.version}</version>
</dependency>
<!-- LiteFlow 规则编排引擎 -->
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-spring-boot-starter</artifactId>
<version>${liteflow.version}</version>
</dependency>
<!-- mqtt客户端 -->
<dependency>
<groupId>org.dromara.mica-mqtt</groupId>
@@ -388,7 +396,7 @@
<!-- JustAuth 的依赖配置-->
<dependency>
<groupId>me.zhyd.oauth</groupId>
<groupId>cn.herodotus.opensteward</groupId>
<artifactId>JustAuth</artifactId>
<version>${justauth.version}</version>
</dependency>
+2 -2
View File
@@ -1,6 +1,6 @@
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
FROM bellsoft/liberica-openjdk-rocky:21.0.11-cds
# FROM bellsoft/liberica-openjdk-rocky:25.0.3-cds
FROM bellsoft/liberica-openjdk-rocky:21.0.12-cds
# FROM bellsoft/liberica-openjdk-rocky:25.0.4-cds
# FROM findepi/graalvm:java21-native
LABEL maintainer="Lion Li"
+4
View File
@@ -143,6 +143,10 @@
<artifactId>ruoyi-gen</artifactId>
</dependency>
</dependencies>
<activation>
<!-- 默认激活 -->
<activeByDefault>true</activeByDefault>
</activation>
</profile>
</profiles>
@@ -43,7 +43,7 @@ snail-ai:
app-id: 1
# 认证令牌(在 Server「应用管理」页面创建时自动生成)
token: SAI_566a6bfbc26e4998b4841cc927d50c5d
port: 18889
port: 3${server.port}
# Skill 文件临时目录
skill-temp-dir: /tmp/snail-ai-agent/skills
# ==================== Chat 嵌入模式配置 ====================
@@ -55,7 +55,7 @@ snail-ai:
# 嵌入模式下隐藏侧边栏用户信息
show-sidebar-user: false
# 嵌入模式下隐藏智能体市场入口
show-agent-market: false
show-agent-market: true
# 嵌入模式下使用紧凑输入框
compact-input: true
# 是否锁定当前智能体
@@ -46,7 +46,7 @@ snail-ai:
app-id: 1
# 认证令牌(在 Server「应用管理」页面创建时自动生成)
token: SAI_566a6bfbc26e4998b4841cc927d50c5d
port: 18889
port: 3${server.port}
# Skill 文件临时目录
skill-temp-dir: /tmp/snail-ai-agent/skills
# ==================== Chat 嵌入模式配置 ====================
@@ -58,7 +58,7 @@ snail-ai:
# 嵌入模式下隐藏侧边栏用户信息
show-sidebar-user: false
# 嵌入模式下隐藏智能体市场入口
show-agent-market: false
show-agent-market: true
# 嵌入模式下使用紧凑输入框
compact-input: true
# 是否锁定当前智能体
+10 -1
View File
@@ -182,7 +182,7 @@ springdoc:
- group: 3.系统模块
packages-to-scan: org.dromara.system
- group: 4.代码生成模块
packages-to-scan: org.dromara.generator
packages-to-scan: org.dromara.gen
- group: 5.工作流模块
packages-to-scan: org.dromara.workflow
@@ -245,6 +245,15 @@ warm-flow:
# 默认Authorization,如果有多个token,用逗号分隔
token-name: ${sa-token.token-name},clientid
--- # LiteFlow 业务规则编排
liteflow:
# 跟随工作流模块开关,避免禁用工作流时仍加载任务办理链
enable: ${warm-flow.enabled:true}
# 工作流模块用于编排任务办理链路
rule-source: classpath:liteflow/*.el.xml
# 开启虚拟线程
enable-virtual-thread: true
--- # mqtt 配置
# 具体配置还需查看文档
# https://mica-mqtt.dreamlu.net/guide/spring/client.html
+1
View File
@@ -37,6 +37,7 @@
<module>ruoyi-common-json</module>
<module>ruoyi-common-encrypt</module>
<module>ruoyi-common-push</module>
<module>ruoyi-common-liteflow</module>
<module>ruoyi-common-mqtt</module>
<module>ruoyi-common-ai</module>
<module>ruoyi-common-mcp</module>
@@ -1,124 +0,0 @@
package org.dromara.common.ai.handler;
import com.aizuda.snail.ai.agent.chat.starter.SnailAiChatGatewayController;
import com.aizuda.snail.ai.common.execption.BaseSnailAiException;
import com.aizuda.snail.ai.common.execption.SnailAiAuthenticationException;
import com.aizuda.snail.ai.common.model.ModelCallException;
import com.aizuda.snail.ai.common.model.Result;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.HandlerMethodValidationException;
import java.util.Collection;
import java.util.stream.Collectors;
/**
* Snail AI Chat 网关异常处理器。
* <p>
* /api/snail/chat/** 接口由 Snail AI SDK 前端消费,响应结构需要保持 SDK Result(status/message/data)
* 不能落到 RuoYi 通用 R(code/msg/data) 格式。
*/
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
@RestControllerAdvice(assignableTypes = SnailAiChatGatewayController.class)
public class SnailAiChatExceptionHandler {
private static final int AUTHENTICATION_ERROR_STATUS = 5001;
@ExceptionHandler(SnailAiAuthenticationException.class)
public Result<Void> handleAuthenticationException(SnailAiAuthenticationException e) {
log.warn("Snail AI Chat authentication failed: {}", e.getMessage());
return Result.fail(AUTHENTICATION_ERROR_STATUS, defaultMessage(e, "认证失败,请重新登录"));
}
@ExceptionHandler(BaseSnailAiException.class)
public Result<Void> handleSnailAiException(BaseSnailAiException e) {
log.warn("Snail AI Chat request failed: {}", e.getMessage());
return Result.fail(defaultMessage(e, "AI 服务请求失败,请稍后再试"));
}
@ExceptionHandler(ModelCallException.class)
public Result<Void> handleModelCallException(ModelCallException e) {
log.warn("Snail AI Chat model call failed: {}", e.getMessage(), e);
return Result.fail(defaultMessage(e, "模型调用失败,请稍后再试"));
}
@ExceptionHandler({
BindException.class,
MethodArgumentNotValidException.class,
HandlerMethodValidationException.class
})
public Result<Void> handleValidationException(Exception e) {
log.warn("Snail AI Chat validation failed: {}", e.getMessage());
return Result.fail(validationMessage(e));
}
@ExceptionHandler(ConstraintViolationException.class)
public Result<Void> handleConstraintViolationException(ConstraintViolationException e) {
log.warn("Snail AI Chat constraint validation failed: {}", e.getMessage());
String message = e.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.filter(this::hasText)
.collect(Collectors.joining(", "));
return Result.fail(hasText(message) ? message : "请求参数校验失败");
}
@ExceptionHandler(HttpMessageNotReadableException.class)
public Result<Void> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
log.warn("Snail AI Chat request body parse failed: {}", e.getMessage());
return Result.fail("请求参数格式错误");
}
@ExceptionHandler(IllegalArgumentException.class)
public Result<Void> handleIllegalArgumentException(IllegalArgumentException e) {
log.warn("Snail AI Chat illegal argument: {}", e.getMessage());
return Result.fail(defaultMessage(e, "请求参数不合法"));
}
@ExceptionHandler(IllegalStateException.class)
public Result<Void> handleIllegalStateException(IllegalStateException e) {
log.warn("Snail AI Chat illegal state: {}", e.getMessage());
return Result.fail(defaultMessage(e, "AI 会话状态异常,请刷新后重试"));
}
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("Snail AI Chat unexpected exception", e);
return Result.fail("AI 服务异常,请稍后再试");
}
private String validationMessage(Exception e) {
Collection<? extends MessageSourceResolvable> errors;
if (e instanceof BindException bindException) {
errors = bindException.getAllErrors();
} else if (e instanceof MethodArgumentNotValidException methodArgumentNotValidException) {
errors = methodArgumentNotValidException.getBindingResult().getAllErrors();
} else if (e instanceof HandlerMethodValidationException handlerMethodValidationException) {
errors = handlerMethodValidationException.getAllErrors();
} else {
return "请求参数校验失败";
}
String message = errors.stream()
.map(MessageSourceResolvable::getDefaultMessage)
.filter(this::hasText)
.collect(Collectors.joining(", "));
return hasText(message) ? message : "请求参数校验失败";
}
private String defaultMessage(Throwable e, String fallback) {
return hasText(e.getMessage()) ? e.getMessage() : fallback;
}
private boolean hasText(String value) {
return value != null && !value.isBlank();
}
}
+8 -1
View File
@@ -14,7 +14,7 @@
</description>
<properties>
<revision>6.0.0-BETA</revision>
<revision>6.0.0</revision>
</properties>
<dependencyManagement>
@@ -159,6 +159,13 @@
<version>${revision}</version>
</dependency>
<!-- liteflow模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-liteflow</artifactId>
<version>${revision}</version>
</dependency>
<!-- mqtt模块 -->
<dependency>
<groupId>org.dromara</groupId>
@@ -12,7 +12,7 @@ import org.dromara.common.doc.core.customizer.JavadocOperationCustomizer;
import org.dromara.common.doc.core.resolver.JavadocResolver;
import org.dromara.common.doc.core.resolver.SaTokenAnnotationMetadataJavadocResolver;
import org.springdoc.core.configuration.SpringDocConfiguration;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springdoc.core.providers.JavadocProvider;
import org.springdoc.core.utils.PropertyResolverUtils;
import org.springframework.boot.autoconfigure.AutoConfiguration;
@@ -108,7 +108,7 @@ public class SpringDocConfig {
* 对已经生成好的 OpenApi 进行自定义操作
*/
@Bean
public OpenApiCustomizer openApiCustomizer() {
public GlobalOpenApiCustomizer openApiCustomizer() {
String contextPath = serverProperties.getServlet().getContextPath();
String finalContextPath;
if (StringUtils.isBlank(contextPath) || "/".equals(contextPath)) {
@@ -8,8 +8,8 @@ import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springdoc.core.customizers.GlobalOperationCustomizer;
import org.springdoc.core.providers.JavadocProvider;
import org.springdoc.core.service.OpenAPIService;
import org.springdoc.core.utils.PropertyResolverUtils;
@@ -29,7 +29,7 @@ import java.util.stream.Stream;
* @author Lion Li
*/
@RequiredArgsConstructor
public class ClassTagOperationCustomizer implements OperationCustomizer, OpenApiCustomizer {
public class ClassTagOperationCustomizer implements GlobalOperationCustomizer, GlobalOpenApiCustomizer {
/**
* JavaDoc 提供器。
@@ -4,7 +4,7 @@ import io.swagger.v3.oas.models.Operation;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.dromara.common.doc.core.resolver.JavadocResolver;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springdoc.core.customizers.GlobalOperationCustomizer;
import org.springdoc.core.providers.JavadocProvider;
import org.springframework.util.CollectionUtils;
import org.springframework.web.method.HandlerMethod;
@@ -18,7 +18,7 @@ import java.util.Optional;
* @author Lion Li
*/
@RequiredArgsConstructor
public class JavadocOperationCustomizer implements OperationCustomizer {
public class JavadocOperationCustomizer implements GlobalOperationCustomizer {
/**
* JavaDoc 提供器。
@@ -23,11 +23,11 @@ import java.util.Base64;
public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
private static final Charset RESPONSE_CHARSET = StandardCharsets.UTF_8;
private final ByteArrayOutputStream byteArrayOutputStream;
private final ServletOutputStream servletOutputStream;
private PrintWriter printWriter;
private Charset charset;
/**
* 构造加密响应包装器。
@@ -39,7 +39,6 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
super(response);
this.byteArrayOutputStream = new ByteArrayOutputStream();
this.servletOutputStream = this.getOutputStream();
this.charset = resolveCharset(response);
}
/**
@@ -50,8 +49,7 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
@Override
public PrintWriter getWriter() {
if (printWriter == null) {
charset = resolveCharset((HttpServletResponse) getResponse());
printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream, charset));
printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream, RESPONSE_CHARSET));
}
return printWriter;
}
@@ -106,7 +104,7 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
*/
public String getContent() throws IOException {
flushBuffer();
return byteArrayOutputStream.toString(charset);
return byteArrayOutputStream.toString(RESPONSE_CHARSET);
}
/**
@@ -130,14 +128,14 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
// vue版本需要设置
servletResponse.addHeader("Access-Control-Expose-Headers", headerFlag);
servletResponse.setHeader(headerFlag, encryptPassword);
servletResponse.setCharacterEncoding(charset.name());
servletResponse.setCharacterEncoding(RESPONSE_CHARSET.name());
// 获取原始内容
String originalBody = this.getContent();
// 对内容进行加密
String encryptContent = EncryptUtils.encryptByAes(originalBody, aesPassword);
servletResponse.setContentLengthLong(encryptContent.getBytes(charset).length);
servletResponse.setContentLengthLong(encryptContent.getBytes(RESPONSE_CHARSET).length);
return encryptContent;
}
@@ -206,20 +204,6 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
};
}
/**
* 解析响应字符集,未设置时默认使用 UTF-8。
*
* @param response 原始响应
* @return 响应字符集
*/
private Charset resolveCharset(HttpServletResponse response) {
String characterEncoding = response.getCharacterEncoding();
if (characterEncoding == null) {
return StandardCharsets.UTF_8;
}
return Charset.forName(characterEncoding);
}
/**
* 生成响应内容 AES 加密密钥。
*
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common-liteflow</artifactId>
<description>
ruoyi-common-liteflow LiteFlow规则编排模块
</description>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<!-- LiteFlow 规则编排 -->
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,19 @@
package org.dromara.common.liteflow.component;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeBooleanComponent;
/**
* LiteFlow 恒为 false 的条件节点
*
* @author Lion Li
*/
@LiteflowComponent("alwaysFalse")
public class AlwaysFalseComponent extends NodeBooleanComponent {
@Override
public boolean processBoolean() {
return false;
}
}
@@ -0,0 +1,19 @@
package org.dromara.common.liteflow.component;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeBooleanComponent;
/**
* LiteFlow 恒为 true 的条件节点
*
* @author Lion Li
*/
@LiteflowComponent("alwaysTrue")
public class AlwaysTrueComponent extends NodeBooleanComponent {
@Override
public boolean processBoolean() {
return true;
}
}
@@ -0,0 +1,22 @@
package org.dromara.common.liteflow.component;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import org.dromara.common.core.exception.ServiceException;
/**
* LiteFlow 上下文必填校验节点
*
* @author Lion Li
*/
@LiteflowComponent("contextRequired")
public class ContextRequiredComponent extends NodeComponent {
@Override
public void process() {
if (getFirstContextBean() == null) {
throw new ServiceException("LiteFlow 上下文不能为空");
}
}
}
@@ -0,0 +1,40 @@
package org.dromara.common.liteflow.component;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.liteflow.core.FailMessageProvider;
/**
* LiteFlow 失败节点用于显式表达链路分支不可继续执行
*
* @author Lion Li
*/
@LiteflowComponent("fail")
public class FailComponent extends NodeComponent {
private static final String DEFAULT_MESSAGE = "LiteFlow 链路执行失败";
@Override
public void process() {
throw new ServiceException(getFailMessage());
}
/**
* 优先从上下文读取业务失败提示未提供时使用默认提示
*
* @return 失败提示
*/
private String getFailMessage() {
Object context = getFirstContextBean();
if (context instanceof FailMessageProvider provider) {
String message = provider.getFailMessage();
if (StringUtils.isNotBlank(message)) {
return message;
}
}
return DEFAULT_MESSAGE;
}
}
@@ -0,0 +1,19 @@
package org.dromara.common.liteflow.component;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
/**
* LiteFlow 空节点用于显式表达无需处理的分支
*
* @author Lion Li
*/
@LiteflowComponent("noop")
public class NoopComponent extends NodeComponent {
@Override
public void process() {
// no-op
}
}
@@ -0,0 +1,67 @@
package org.dromara.common.liteflow.config;
import org.dromara.common.liteflow.component.*;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
/**
* LiteFlow 公共节点自动配置
*
* @author Lion Li
*/
@AutoConfiguration
@ConditionalOnProperty(value = "liteflow.enable", havingValue = "true")
public class LiteFlowAutoConfiguration {
/**
* 空节点用于显式表达无需处理的分支
*
* @return 空节点
*/
@Bean("noop")
public NoopComponent noopComponent() {
return new NoopComponent();
}
/**
* 失败节点用于显式抛出业务失败提示
*
* @return 失败节点
*/
@Bean("fail")
public FailComponent failComponent() {
return new FailComponent();
}
/**
* 上下文必填校验节点
*
* @return 上下文必填校验节点
*/
@Bean("contextRequired")
public ContextRequiredComponent contextRequiredComponent() {
return new ContextRequiredComponent();
}
/**
* 恒为 true 的条件节点
*
* @return true 条件节点
*/
@Bean("alwaysTrue")
public AlwaysTrueComponent alwaysTrueComponent() {
return new AlwaysTrueComponent();
}
/**
* 恒为 false 的条件节点
*
* @return false 条件节点
*/
@Bean("alwaysFalse")
public AlwaysFalseComponent alwaysFalseComponent() {
return new AlwaysFalseComponent();
}
}
@@ -0,0 +1,17 @@
package org.dromara.common.liteflow.core;
/**
* LiteFlow 失败提示提供者
*
* @author Lion Li
*/
public interface FailMessageProvider {
/**
* 获取公共失败节点抛出的业务提示
*
* @return 失败提示
*/
String getFailMessage();
}
@@ -0,0 +1,42 @@
package org.dromara.common.liteflow.utils;
import com.yomahub.liteflow.core.FlowExecutor;
import com.yomahub.liteflow.flow.LiteflowResponse;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.SpringUtils;
/**
* LiteFlow 执行工具
*
* @author Lion Li
*/
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class LiteFlowUtils {
/**
* 执行 LiteFlow 链路并按 contextBean 语义透传业务上下文和失败原因
*
* @param chainId 链路标识
* @param context 链路上下文
*/
public static void execute(String chainId, Object context) {
if (context == null) {
throw new ServiceException("LiteFlow 上下文不能为空");
}
LiteflowResponse response = SpringUtils.getBean(FlowExecutor.class).execute2Resp(chainId, null, context);
if (!response.isSuccess()) {
Exception cause = response.getCause();
log.error("LiteFlow 链路执行失败 chainId={} requestId={} message={} steps={}",
chainId, response.getRequestId(), response.getMessage(), response.getExecuteStepStrWithTime(), cause);
if (cause instanceof RuntimeException runtimeException) {
throw runtimeException;
}
throw new ServiceException(cause != null ? cause.getMessage() : response.getMessage());
}
}
}
@@ -0,0 +1 @@
org.dromara.common.liteflow.config.LiteFlowAutoConfiguration
@@ -8,7 +8,6 @@ import com.baomidou.mybatisplus.core.injector.methods.SelectList;
import com.baomidou.mybatisplus.core.mapper.Mapper;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.toolkit.ArrayUtils;
import com.github.yulichang.adapter.base.tookit.VersionUtils;
import com.github.yulichang.base.JoinMapper;
import com.github.yulichang.method.*;
import com.github.yulichang.toolkit.MPJTableMapperHelper;
@@ -113,23 +112,13 @@ public class MPJSqlInjector extends DefaultSqlInjector {
*/
private List<AbstractMethod> getJoinMethod() {
List<AbstractMethod> list = new ArrayList<>();
if (VersionUtils.compare(VersionUtils.getVersion(), "3.5.0") >= 0) {
list.add(new DeleteJoin(SqlMethod.DELETE_JOIN.getMethod()));
list.add(new UpdateJoin(SqlMethod.UPDATE_JOIN.getMethod()));
list.add(new UpdateJoinAndNull(SqlMethod.UPDATE_JOIN_AND_NULL.getMethod()));
list.add(new SelectJoinCount(SqlMethod.SELECT_JOIN_COUNT.getMethod()));
list.add(new SelectJoinOne(SqlMethod.SELECT_JOIN_ONE.getMethod()));
list.add(new SelectJoinList(SqlMethod.SELECT_JOIN_LIST.getMethod()));
list.add(new SelectJoinPage(SqlMethod.SELECT_JOIN_PAGE.getMethod()));
} else {
list.add(new DeleteJoin());
list.add(new UpdateJoin());
list.add(new UpdateJoinAndNull());
list.add(new SelectJoinCount());
list.add(new SelectJoinOne());
list.add(new SelectJoinList());
list.add(new SelectJoinPage());
}
list.add(new DeleteJoin(SqlMethod.DELETE_JOIN.getMethod()));
list.add(new UpdateJoin(SqlMethod.UPDATE_JOIN.getMethod()));
list.add(new UpdateJoinAndNull(SqlMethod.UPDATE_JOIN_AND_NULL.getMethod()));
list.add(new SelectJoinCount(SqlMethod.SELECT_JOIN_COUNT.getMethod()));
list.add(new SelectJoinOne(SqlMethod.SELECT_JOIN_ONE.getMethod()));
list.add(new SelectJoinList(SqlMethod.SELECT_JOIN_LIST.getMethod()));
list.add(new SelectJoinPage(SqlMethod.SELECT_JOIN_PAGE.getMethod()));
return list;
}
@@ -12,6 +12,7 @@ import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.system.api.model.LoginUser;
import java.time.LocalDateTime;
import java.util.Date;
/**
* MP注入处理器
@@ -61,6 +62,9 @@ public class InjectionMetaObjectHandler implements MetaObjectHandler {
LocalDateTime date = LocalDateTime.now();
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, date);
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, date);
Date legacyDate = new Date();
this.strictInsertFill(metaObject, "createTime", Date.class, legacyDate);
this.strictInsertFill(metaObject, "updateTime", Date.class, legacyDate);
}
} catch (Exception e) {
throw new ServiceException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_INTERNAL_ERROR);
@@ -86,6 +90,7 @@ public class InjectionMetaObjectHandler implements MetaObjectHandler {
baseEntity.setUpdateBy(userId);
} else {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
}
} catch (Exception e) {
throw new ServiceException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_INTERNAL_ERROR);
@@ -20,14 +20,16 @@ public class PlusPostInitTableInfoHandler implements PostInitTableInfoHandler {
*
* @param tableInfo 表信息
* @param configuration MyBatis 配置
* @return 调整后的表信息
*/
@Override
public void postTableInfo(TableInfo tableInfo, Configuration configuration) {
public TableInfo postTableInfo(TableInfo tableInfo, Configuration configuration) {
String flag = SpringUtils.getProperty("mybatis-plus.enableLogicDelete", "true");
// 只有关闭时 统一设置false 为true时mp自动判断不处理
if (!Convert.toBool(flag)) {
ReflectUtils.setFieldValue(tableInfo, "withLogicDelete", false);
}
return tableInfo;
}
}
@@ -15,6 +15,14 @@ import software.amazon.awssdk.core.ResponseInputStream;
import software.amazon.awssdk.core.async.AsyncRequestBody;
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
import software.amazon.awssdk.core.async.ResponsePublisher;
import software.amazon.awssdk.http.SdkHttpFullRequest;
import software.amazon.awssdk.http.SdkHttpMethod;
import software.amazon.awssdk.http.auth.aws.signer.AwsV4FamilyHttpSigner;
import software.amazon.awssdk.http.auth.aws.signer.AwsV4HttpSigner;
import software.amazon.awssdk.http.auth.spi.signer.HttpSigner;
import software.amazon.awssdk.http.auth.spi.signer.SignRequest;
import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
@@ -25,6 +33,7 @@ import software.amazon.awssdk.transfer.s3.S3TransferManager;
import software.amazon.awssdk.transfer.s3.model.CompletedUpload;
import software.amazon.awssdk.transfer.s3.model.DownloadRequest;
import software.amazon.awssdk.transfer.s3.progress.TransferListener;
import software.amazon.awssdk.utils.http.SdkHttpUtils;
import java.io.*;
import java.nio.channels.Channels;
@@ -33,8 +42,11 @@ import java.nio.channels.SeekableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.net.URI;
import java.util.*;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
@@ -716,6 +728,9 @@ public abstract class AbstractOssClientImpl implements OssClient {
*/
@Override
public String bucketPresignGetUrl(String bucket, String key, Duration expiredTime) {
if (useBucketBoundDomain(bucket)) {
return bucketBoundDomainPresignUrl(SdkHttpMethod.GET, key, expiredTime, Collections.emptyMap());
}
try {
return s3Presigner.presignGetObject(getObjectPresignRequestBuilder -> {
getObjectPresignRequestBuilder.signatureDuration(expiredTime)
@@ -739,6 +754,9 @@ public abstract class AbstractOssClientImpl implements OssClient {
*/
@Override
public String bucketPresignPutUrl(String bucket, String key, Duration expiredTime, Map<String, String> metadata) {
if (useBucketBoundDomain(bucket)) {
return bucketBoundDomainPresignUrl(SdkHttpMethod.PUT, key, expiredTime, metadata);
}
try {
return s3Presigner.presignPutObject(putObjectPresignRequestBuilder -> {
putObjectPresignRequestBuilder.signatureDuration(expiredTime)
@@ -751,6 +769,94 @@ public abstract class AbstractOssClientImpl implements OssClient {
}
}
/**
* 是否使用已绑定默认桶的自定义域名生成无桶名预签名 URL
*
* @param bucket 存储桶名称
* @return 是否使用自定义域名签名
*/
private boolean useBucketBoundDomain(String bucket) {
return config.domain()
.filter(StringUtils::isNotBlank)
.isPresent() && config.bucket()
.filter(defaultBucket -> Objects.equals(defaultBucket, bucket))
.isPresent();
}
/**
* 使用绑定默认桶的自定义域名生成预签名 URL避免 S3 SDK 自动拼接桶名
*
* @param method HTTP 方法
* @param key 对象键
* @param expiredTime 过期时间
* @param metadata 对象元数据
* @return 预签名 URL
*/
private String bucketBoundDomainPresignUrl(SdkHttpMethod method, String key, Duration expiredTime, Map<String, String> metadata) {
try {
URI domainUri = URI.create(config.getDomainUrl());
SdkHttpFullRequest.Builder requestBuilder = SdkHttpFullRequest.builder()
.method(method)
.uri(domainUri)
.encodedPath(bucketBoundDomainPath(domainUri, key));
if (metadata != null) {
metadata.forEach((metadataKey, metadataValue) -> {
if (StringUtils.isNotBlank(metadataKey)) {
requestBuilder.putHeader("x-amz-meta-" + metadataKey, String.valueOf(metadataValue));
}
});
}
AwsCredentialsIdentity credentials = AwsCredentialsIdentity.create(
config.accessKey()
.filter(StringUtils::isNotBlank)
.orElseThrow(() -> S3StorageException.form("accessKey is not configured.")),
config.secretKey()
.filter(StringUtils::isNotBlank)
.orElseThrow(() -> S3StorageException.form("secretKey is not configured."))
);
Clock signingClock = Clock.fixed(Instant.now(), ZoneOffset.UTC);
return AwsV4HttpSigner.create()
.sign(SignRequest.builder(credentials)
.request(requestBuilder.build())
.putProperty(AwsV4HttpSigner.REGION_NAME, config.region().orElse(Region.US_EAST_1).id())
.putProperty(AwsV4FamilyHttpSigner.SERVICE_SIGNING_NAME, "s3")
.putProperty(AwsV4FamilyHttpSigner.AUTH_LOCATION, AwsV4FamilyHttpSigner.AuthLocation.QUERY_STRING)
.putProperty(AwsV4FamilyHttpSigner.PAYLOAD_SIGNING_ENABLED, false)
.putProperty(AwsV4FamilyHttpSigner.EXPIRATION_DURATION, expiredTime)
.putProperty(HttpSigner.SIGNING_CLOCK, signingClock)
.putProperty(AwsV4FamilyHttpSigner.DOUBLE_URL_ENCODE, false)
.putProperty(AwsV4FamilyHttpSigner.NORMALIZE_PATH, false)
.build())
.request()
.getUri()
.toString();
} catch (Exception e) {
throw toStorageException(e);
}
}
/**
* 构建绑定桶域名下的对象访问路径
*
* @param domainUri 自定义域名 URI
* @param key 对象键
* @return 编码后的访问路径
*/
private String bucketBoundDomainPath(URI domainUri, String key) {
String basePath = Optional.ofNullable(domainUri.getRawPath())
.filter(StringUtils::isNotBlank)
.filter(path -> !"/".equals(path))
.orElse("");
String objectPath = SdkHttpUtils.urlEncodeIgnoreSlashes(key);
if (!basePath.startsWith("/")) {
basePath = "/" + basePath;
}
if (!basePath.endsWith("/")) {
basePath = basePath + "/";
}
return basePath + objectPath;
}
/**
* 上传本地路径文件到默认存储桶
*
@@ -1,6 +1,5 @@
package org.dromara.common.oss.config;
import cn.hutool.http.HttpUtil;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
@@ -196,8 +195,8 @@ public class OssClientConfig implements Config<OssClientConfig, OssClientConfig.
public String getDomainUrl() {
return domain()
// 如果已经配置了自定义域名则优先使用域名
// 检查携带协议头
.filter(OssClientConfig::hasHttpHeader)
.filter(StringUtils::isNotBlank)
.map(domain -> BucketUrlUtil.rebuildUrlHeader(useHttps, domain.trim()))
// 否则使用站点
.orElseGet(this::getEndpointUrl);
}
@@ -274,7 +273,8 @@ public class OssClientConfig implements Config<OssClientConfig, OssClientConfig.
*/
private String getAccessBaseUrl() {
return domain()
.filter(OssClientConfig::hasHttpHeader)
.filter(StringUtils::isNotBlank)
.map(String::trim)
.orElseGet(this::getEndpoint);
}
@@ -289,16 +289,6 @@ public class OssClientConfig implements Config<OssClientConfig, OssClientConfig.
.orElseThrow(() -> S3StorageException.form("endpoint is not configured."));
}
/**
* 判断 URL 是否包含 HTTP 协议头
*
* @param url URL
* @return 是否包含 HTTP 协议头
*/
private static boolean hasHttpHeader(String url) {
return HttpUtil.isHttp(url) || HttpUtil.isHttps(url);
}
/**
* ACL访问策略配置
*/
@@ -31,4 +31,9 @@ public interface MessageConstants {
* 心跳响应标识
*/
String PONG = "pong";
/**
* 同一 token 的新连接替换旧连接时发送给旧连接的控制消息
*/
String KICKED = "kicked";
}
@@ -65,6 +65,7 @@ public class SseEmitterSessionManager implements PushSessionManager {
// 关闭已存在的SseEmitter防止超过最大连接数
SseEmitter oldEmitter = emitters.remove(token);
if (oldEmitter != null) {
sendKickedMessage(oldEmitter);
oldEmitter.complete();
}
@@ -103,6 +104,21 @@ public class SseEmitterSessionManager implements PushSessionManager {
return emitter;
}
/**
* 通知旧连接已被同 token 新连接替换
*
* @param emitter SSE 连接
*/
private void sendKickedMessage(SseEmitter emitter) {
try {
emitter.send(SseEmitter.event()
.name("message")
.data(MessageConstants.KICKED));
} catch (Exception ignore) {
// 旧连接可能已断开忽略通知失败
}
}
/**
* 断开指定用户的 SSE 连接
*
@@ -5,6 +5,7 @@ import cn.hutool.core.map.MapUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.ThreadUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.push.constant.MessageConstants;
import org.dromara.common.push.dto.PushDTO;
import org.dromara.common.push.properties.MessageProperties;
import org.dromara.common.redis.utils.RedisUtils;
@@ -61,6 +62,7 @@ public class WebSocketSessionManager implements PushSessionManager {
Map<String, WebSocketSession> sessions = USER_TOKEN_SESSIONS.computeIfAbsent(userId, key -> new ConcurrentHashMap<>());
// 移除并关闭旧的同token会话避免重复连接
WebSocketSession oldSession = sessions.remove(token);
sendKickedMessage(oldSession);
closeSession(oldSession, CloseStatus.NORMAL);
// 存储新会话
sessions.put(token, session);
@@ -129,6 +131,18 @@ public class WebSocketSessionManager implements PushSessionManager {
toRemoveUsers.forEach(USER_TOKEN_SESSIONS::remove);
}
/**
* 通知旧连接已被同 token 新连接替换
*
* @param session WebSocket 会话
*/
private void sendKickedMessage(WebSocketSession session) {
if (session == null || !session.isOpen()) {
return;
}
sendMessage(session, MessageConstants.KICKED);
}
/**
* 订阅消息通道
* 注册消息消费者监听Redis消息推送
@@ -0,0 +1,32 @@
package org.dromara.common.redis.config;
import cn.hutool.http.HttpStatus;
import com.baomidou.lock.LockFailureStrategy;
import com.baomidou.lock.spring.boot.autoconfigure.LockAutoConfiguration;
import org.dromara.common.core.exception.ServiceException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Role;
/**
* Lock4j 配置
*
* @author AprilWind
*/
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@AutoConfiguration(before = LockAutoConfiguration.class)
public class Lock4jConfig {
/**
* Lock4j 获取锁失败策略
*/
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public LockFailureStrategy defaultLockFailureStrategy() {
return (key, method, arguments) -> {
throw new ServiceException("业务处理中,请稍后再试...", HttpStatus.HTTP_UNAVAILABLE);
};
}
}
@@ -2,3 +2,4 @@ org.dromara.common.redis.config.RedisConfig
org.dromara.common.redis.config.CacheConfig
org.dromara.common.redis.config.IdempotentConfig
org.dromara.common.redis.config.RateLimiterConfig
org.dromara.common.redis.config.Lock4jConfig
@@ -11,6 +11,10 @@
<artifactId>ruoyi-common-satoken</artifactId>
<description>
ruoyi-common-satoken 权限认证
</description>
<dependencies>
<!-- 核心模块 -->
@@ -46,10 +46,10 @@ public class SaTokenExceptionHandler {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, e.getMessage());
String msg = switch (e.getType()) {
case NotLoginException.TOKEN_TIMEOUT -> "登录已过期,请重新登录";
case NotLoginException.TOKEN_TIMEOUT,
NotLoginException.TOKEN_FREEZE -> "登录已过期,请重新登录";
case NotLoginException.BE_REPLACED -> "当前账号已在其他设备登录,您已被强制下线";
case NotLoginException.KICK_OUT -> "账号已被管理员强制下线";
case NotLoginException.TOKEN_FREEZE -> "账号已被冻结,请联系管理员处理";
default -> "登录状态异常,请重新登录";
};
return R.fail(HttpStatus.HTTP_UNAUTHORIZED, msg);
+8 -1
View File
@@ -18,10 +18,17 @@
<dependencies>
<!-- 授权认证 -->
<dependency>
<groupId>me.zhyd.oauth</groupId>
<groupId>cn.herodotus.opensteward</groupId>
<artifactId>JustAuth</artifactId>
</dependency>
<!-- 仅供 justauth 内部解析参数使用 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.60</version>
</dependency>
<!-- 序列化模块 -->
<dependency>
<groupId>org.dromara</groupId>
@@ -1,9 +1,10 @@
package me.zhyd.oauth.request;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson2.JSONObject;
import me.zhyd.oauth.cache.AuthStateCache;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.config.AuthSource;
import me.zhyd.oauth.constant.Keys;
import me.zhyd.oauth.enums.AuthResponseStatus;
import me.zhyd.oauth.enums.AuthUserGender;
import me.zhyd.oauth.exception.AuthException;
@@ -59,8 +60,8 @@ public abstract class AbstractAuthWeChatEnterpriseRequest extends AuthDefaultReq
JSONObject object = this.checkResponse(response);
return AuthToken.builder()
.accessToken(object.getString("access_token"))
.expireIn(object.getIntValue("expires_in"))
.accessToken(object.getString(Keys.OAUTH2_ACCESS_TOKEN))
.expireIn(object.getIntValue(Keys.OAUTH2_EXPIRES_IN))
.code(authCallback.getCode())
.build();
}
@@ -78,22 +79,27 @@ public abstract class AbstractAuthWeChatEnterpriseRequest extends AuthDefaultReq
// 返回 OpenId 或其他均代表非当前企业用户不支持
// https://github.com/justauth/JustAuth/issues/227 修复bug
if (!object.containsKey("userid")) {
String userId = null;
if (object.containsKey("userid")) {
userId = object.getString("userid");
} else if (object.containsKey("UserId")) {
userId = object.getString("UserId");
}
if (userId == null) {
throw new AuthException(AuthResponseStatus.UNIDENTIFIED_PLATFORM, source);
}
String userId = object.getString("userid");
String userTicket = object.getString("user_ticket");
JSONObject userDetail = getUserDetail(authToken.getAccessToken(), userId, userTicket);
return AuthUser.builder()
.rawUserInfo(userDetail)
.username(userDetail.getString("name"))
.username(userDetail.getString(Keys.NAME))
.nickname(userDetail.getString("alias"))
.avatar(userDetail.getString("avatar"))
.location(userDetail.getString("address"))
.email(userDetail.getString("email"))
.avatar(userDetail.getString(Keys.AVATAR))
.location(userDetail.getString(Keys.OAUTH2_SCOPE__ADDRESS))
.email(userDetail.getString(Keys.OAUTH2_SCOPE__EMAIL))
.uuid(userId)
.gender(AuthUserGender.getWechatRealGender(userDetail.getString("gender")))
.gender(AuthUserGender.getWechatRealGender(userDetail.getString(Keys.GENDER)))
.token(authToken)
.source(source.toString())
.build();
@@ -108,8 +114,8 @@ public abstract class AbstractAuthWeChatEnterpriseRequest extends AuthDefaultReq
private JSONObject checkResponse(String response) {
JSONObject object = JSONObject.parseObject(response);
if (object.containsKey("errcode") && object.getIntValue("errcode") != 0) {
throw new AuthException(object.getString("errmsg"), source);
if (object.containsKey(Keys.VARIANT__ERRCODE) && object.getIntValue(Keys.VARIANT__ERRCODE) != 0) {
throw new AuthException(object.getString(Keys.VARIANT__ERRMSG), source);
}
return object;
@@ -139,8 +145,8 @@ public abstract class AbstractAuthWeChatEnterpriseRequest extends AuthDefaultReq
@Override
protected String userInfoUrl(AuthToken authToken) {
return UrlBuilder.fromBaseUrl(source.userInfo())
.queryParam("access_token", authToken.getAccessToken())
.queryParam("code", authToken.getCode())
.queryParam(Keys.OAUTH2_ACCESS_TOKEN, authToken.getAccessToken())
.queryParam(Keys.OAUTH2_CODE, authToken.getCode())
.build();
}
@@ -155,8 +161,8 @@ public abstract class AbstractAuthWeChatEnterpriseRequest extends AuthDefaultReq
private JSONObject getUserDetail(String accessToken, String userId, String userTicket) {
// 用户基础信息
String userInfoUrl = UrlBuilder.fromBaseUrl("https://qyapi.weixin.qq.com/cgi-bin/user/get")
.queryParam("access_token", accessToken)
.queryParam("userid", userId)
.queryParam(Keys.OAUTH2_ACCESS_TOKEN, accessToken)
.queryParam(Keys.USERID, userId)
.build();
String userInfoResponse = new HttpUtils(config.getHttpConfig()).get(userInfoUrl).getBody();
JSONObject userInfo = checkResponse(userInfoResponse);
@@ -164,7 +170,7 @@ public abstract class AbstractAuthWeChatEnterpriseRequest extends AuthDefaultReq
// 用户敏感信息
if (StringUtils.isNotEmpty(userTicket)) {
String userDetailUrl = UrlBuilder.fromBaseUrl("https://qyapi.weixin.qq.com/cgi-bin/auth/getuserdetail")
.queryParam("access_token", accessToken)
.queryParam(Keys.OAUTH2_ACCESS_TOKEN, accessToken)
.build();
JSONObject param = new JSONObject();
param.put("user_ticket", userTicket);
@@ -1,17 +1,17 @@
package me.zhyd.oauth.request;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson2.JSONObject;
import com.xkcoding.http.support.HttpHeader;
import me.zhyd.oauth.cache.AuthStateCache;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.config.AuthDefaultSource;
import me.zhyd.oauth.constant.Keys;
import me.zhyd.oauth.enums.scope.AuthDingTalkScope;
import me.zhyd.oauth.exception.AuthException;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.utils.AuthScopeUtils;
import me.zhyd.oauth.utils.GlobalAuthUtils;
import me.zhyd.oauth.utils.HttpUtils;
import me.zhyd.oauth.utils.UrlBuilder;
@@ -54,16 +54,16 @@ public class AuthDingTalkV2Request extends AuthDefaultRequest {
@Override
public String authorize(String state) {
return UrlBuilder.fromBaseUrl(source.authorize())
.queryParam("response_type", "code")
.queryParam("client_id", config.getClientId())
.queryParam("scope", this.getScopes(",", true, AuthScopeUtils.getDefaultScopes(AuthDingTalkScope.values())))
.queryParam("redirect_uri", GlobalAuthUtils.urlEncode(config.getRedirectUri()))
.queryParam(Keys.OAUTH2_RESPONSE_TYPE, Keys.OAUTH2_CODE)
.queryParam(Keys.OAUTH2_CLIENT_ID, config.getClientId())
.queryParam(Keys.OAUTH2_SCOPE, this.getScopes(",", true, AuthScopeUtils.getDefaultScopes(AuthDingTalkScope.values())))
.queryParam(Keys.OAUTH2_REDIRECT_URI, config.getRedirectUri())
.queryParam("prompt", "consent")
.queryParam("org_type", config.getDingTalkOrgType())
.queryParam("corpId", config.getDingTalkCorpId())
.queryParam("exclusiveLogin", config.isDingTalkExclusiveLogin())
.queryParam("exclusiveCorpId", config.getDingTalkExclusiveCorpId())
.queryParam("state", getRealState(state))
.queryParam(Keys.OAUTH2_STATE, getRealState(state))
.build();
}
@@ -76,10 +76,10 @@ public class AuthDingTalkV2Request extends AuthDefaultRequest {
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
Map<String, String> params = new HashMap<>();
params.put("grantType", "authorization_code");
params.put("grantType", Keys.OAUTH2_GRANT_TYPE__AUTHORIZATION_CODE);
params.put("clientId", config.getClientId());
params.put("clientSecret", config.getClientSecret());
params.put("code", authCallback.getCode());
params.put(Keys.OAUTH2_CODE, authCallback.getCode());
String response = new HttpUtils(config.getHttpConfig()).post(this.source.accessToken(), JSONObject.toJSONString(params)).getBody();
JSONObject accessTokenObject = JSONObject.parseObject(response);
if (!accessTokenObject.containsKey("accessToken")) {
@@ -129,10 +129,10 @@ public class AuthDingTalkV2Request extends AuthDefaultRequest {
*/
protected String accessTokenUrl(String code) {
return UrlBuilder.fromBaseUrl(source.accessToken())
.queryParam("code", code)
.queryParam(Keys.OAUTH2_CODE, code)
.queryParam("clientId", config.getClientId())
.queryParam("clientSecret", config.getClientSecret())
.queryParam("grantType", "authorization_code")
.queryParam("grantType", Keys.OAUTH2_GRANT_TYPE__AUTHORIZATION_CODE)
.build();
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
FROM bellsoft/liberica-openjdk-rocky:21.0.11-cds
# FROM bellsoft/liberica-openjdk-rocky:25.0.3-cds
FROM bellsoft/liberica-openjdk-rocky:21.0.12-cds
# FROM bellsoft/liberica-openjdk-rocky:25.0.4-cds
# FROM findepi/graalvm:java21-native
LABEL maintainer="Lion Li"
+2 -2
View File
@@ -1,6 +1,6 @@
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
FROM bellsoft/liberica-openjdk-rocky:21.0.11-cds
# FROM bellsoft/liberica-openjdk-rocky:25.0.3-cds
FROM bellsoft/liberica-openjdk-rocky:21.0.12-cds
# FROM bellsoft/liberica-openjdk-rocky:25.0.4-cds
# FROM findepi/graalvm:java21-native
LABEL maintainer="Lion Li"
@@ -17,7 +17,7 @@ public class SnailAiServerApplication {
* @param args 启动参数
*/
public static void main(String[] args) {
com.aizuda.snail.ai.starter.SnailAiSpringbootApplication.main(args);
com.aizuda.snail.ai.starter.SnailAiApplication.main(args);
}
}
@@ -35,6 +35,24 @@ snail-ai:
access-key: minioadmin
secret-key: minioadmin
bucket: snail-ai
# RAG 文档解析与图片 OCR 配置
rag:
docling:
enabled: ${SNAIL_AI_RAG_DOCLING_ENABLED:true}
url: ${SNAIL_AI_RAG_DOCLING_URL:http://127.0.0.1:5100}
timeout-seconds: ${SNAIL_AI_RAG_DOCLING_TIMEOUT_SECONDS:300}
concurrency: ${SNAIL_AI_RAG_DOCLING_CONCURRENCY:3}
health-timeout-millis: ${SNAIL_AI_RAG_DOCLING_HEALTH_TIMEOUT_MILLIS:3000}
poll-interval-millis: ${SNAIL_AI_RAG_DOCLING_POLL_INTERVAL_MILLIS:3000}
status-timeout-millis: ${SNAIL_AI_RAG_DOCLING_STATUS_TIMEOUT_MILLIS:10000}
result-timeout-millis: ${SNAIL_AI_RAG_DOCLING_RESULT_TIMEOUT_MILLIS:30000}
max-image-count: ${SNAIL_AI_RAG_DOCLING_MAX_IMAGE_COUNT:100}
max-image-bytes: ${SNAIL_AI_RAG_DOCLING_MAX_IMAGE_BYTES:10485760}
paddle-ocr-enabled: ${SNAIL_AI_RAG_DOCLING_PADDLE_OCR_ENABLED:true}
paddle-ocr-url: ${SNAIL_AI_RAG_DOCLING_PADDLE_OCR_URL:http://127.0.0.1:8866/ocr}
paddle-ocr-timeout-millis: ${SNAIL_AI_RAG_DOCLING_PADDLE_OCR_TIMEOUT_MILLIS:180000}
paddle-ocr-batch-size: ${SNAIL_AI_RAG_DOCLING_PADDLE_OCR_BATCH_SIZE:5}
vision-ocr-fallback-enabled: ${SNAIL_AI_RAG_DOCLING_VISION_OCR_FALLBACK_ENABLED:false}
# 短期记忆配置
memory:
short-term:
@@ -1,6 +1,6 @@
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
FROM bellsoft/liberica-openjdk-rocky:21.0.11-cds
# FROM bellsoft/liberica-openjdk-rocky:25.0.3-cds
FROM bellsoft/liberica-openjdk-rocky:21.0.12-cds
# FROM bellsoft/liberica-openjdk-rocky:25.0.4-cds
# FROM findepi/graalvm:java21-native
LABEL maintainer="Lion Li"
@@ -11,6 +11,7 @@ import org.dromara.common.core.domain.R;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.web.core.BaseController;
import org.dromara.system.api.model.LoginUser;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -27,6 +28,7 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/snail-ai")
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "snail-ai.open-api", name = "enabled", havingValue = "true")
public class SnailAiController extends BaseController {
/**
@@ -1,14 +1,11 @@
package org.dromara.gen.config.properties;
import cn.hutool.extra.template.TemplateConfig;
import lombok.Data;
import org.dromara.common.core.factory.YmlPropertySourceFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
/**
* 代码生成配置属性
*
@@ -40,9 +37,4 @@ public class GenProperties {
*/
private String tablePrefix;
/**
* 模板配置
*/
private TemplateConfig templateConfig = new TemplateConfig(StandardCharsets.UTF_8, null, TemplateConfig.ResourceMode.CLASSPATH);
}
@@ -85,6 +85,11 @@ public interface GenConstants {
*/
String FRONTEND_TYPE_VUE = "vue";
/**
* React 前端模板类型对应模板根目录下的 react 目录
*/
String FRONTEND_TYPE_REACT = "react";
/**
* 树根节点值
*/
@@ -103,38 +108,39 @@ public interface GenConstants {
/**
* 数据库字符串类型
*/
String[] COLUMNTYPE_STR = {"char", "varchar", "enum", "set", "nchar", "nvarchar", "varchar2", "nvarchar2"};
String[] COLUMNTYPE_STR = {"char", "varchar", "enum", "set", "nchar", "nvarchar", "varchar2", "nvarchar2",
"long varchar", "long nvarchar", "uniqueidentifier", "uuid", "json", "jsonb", "xml"};
/**
* 数据库文本类型
*/
String[] COLUMNTYPE_TEXT = {"tinytext", "text", "mediumtext", "longtext", "binary", "varbinary", "blob",
"ntext", "image", "bytea"};
"ntext", "image", "bytea", "clob", "nclob", "long", "long raw", "raw"};
/**
* 数据库时间类型
*/
String[] COLUMNTYPE_TIME = {"datetime", "time", "date", "timestamp", "year", "interval",
"smalldatetime", "datetime2", "datetimeoffset", "timestamptz"};
"smalldatetime", "datetime2", "datetimeoffset", "timestamptz", "timestamp with time zone",
"timestamp without time zone", "time with time zone", "time without time zone"};
/**
* 数据库数字类型
*/
String[] COLUMNTYPE_NUMBER = {"tinyint", "smallint", "mediumint", "int", "int2", "int4", "int8", "number", "integer",
"bit", "bigint", "float", "float4", "float8", "double", "decimal", "numeric", "real", "double precision",
"smallserial", "serial", "bigserial", "money", "smallmoney"};
"bit", "boolean", "bool", "bigint", "float", "float4", "float8", "double", "decimal", "dec", "fixed",
"numeric", "real", "double precision", "binary_float", "binary_double", "smallserial", "serial",
"serial2", "serial4", "serial8", "bigserial", "money", "smallmoney"};
/**
* BO对象 不需要添加字段
*/
String[] COLUMNNAME_NOT_ADD = {"create_dept", "create_by", "create_time", "del_flag", "update_by",
"update_time", "version"};
String[] COLUMNNAME_NOT_ADD = {"del_flag", "version"};
/**
* BO对象 不需要编辑字段
*/
String[] COLUMNNAME_NOT_EDIT = {"create_dept", "create_by", "create_time", "del_flag", "update_by",
"update_time", "version"};
String[] COLUMNNAME_NOT_EDIT = {"del_flag", "version"};
/**
* VO对象 不需要返回字段
@@ -145,8 +151,7 @@ public interface GenConstants {
/**
* BO对象 不需要查询字段
*/
String[] COLUMNNAME_NOT_QUERY = {"id", "create_dept", "create_by", "create_time", "del_flag", "update_by",
"update_time", "remark", "version"};
String[] COLUMNNAME_NOT_QUERY = {"id", "del_flag", "remark", "version"};
/**
* Entity基类字段
@@ -228,6 +233,11 @@ public interface GenConstants {
*/
String TYPE_DOUBLE = "Double";
/**
* 单精度浮点型
*/
String TYPE_FLOAT = "Float";
/**
* 高精度计算类型
*/
@@ -156,7 +156,7 @@ public class GenTableColumn extends BaseEntity {
}
if (StringUtils.equalsAny(this.javaType,
GenConstants.TYPE_LONG, GenConstants.TYPE_INTEGER, GenConstants.TYPE_DOUBLE,
"Float", GenConstants.TYPE_BIGDECIMAL)) {
GenConstants.TYPE_FLOAT, GenConstants.TYPE_BIGDECIMAL)) {
return "number";
}
if (StringUtils.equals(this.javaType, GenConstants.TYPE_BOOLEAN)) {
@@ -174,7 +174,7 @@ public class GenTableColumn extends BaseEntity {
if (StringUtils.equals(this.javaType, GenConstants.TYPE_BOOLEAN)) {
return "true";
}
if (StringUtils.equalsAny(this.javaType, GenConstants.TYPE_INTEGER, GenConstants.TYPE_LONG)) {
if (isNumberType()) {
return "0";
}
return "'0'";
@@ -189,12 +189,23 @@ public class GenTableColumn extends BaseEntity {
if (StringUtils.equals(this.javaType, GenConstants.TYPE_BOOLEAN)) {
return "false";
}
if (StringUtils.equalsAny(this.javaType, GenConstants.TYPE_INTEGER, GenConstants.TYPE_LONG)) {
if (isNumberType()) {
return "1";
}
return "'1'";
}
/**
* 判断当前字段是否为数值类型
*
* @return 数值类型返回 {@code true}
*/
public boolean isNumberType() {
return StringUtils.equalsAny(this.javaType,
GenConstants.TYPE_INTEGER, GenConstants.TYPE_LONG, GenConstants.TYPE_DOUBLE,
GenConstants.TYPE_FLOAT, GenConstants.TYPE_BIGDECIMAL);
}
/**
* 是否为日期范围查询字段
*
@@ -55,7 +55,7 @@ public class GenTableServiceImpl implements IGenTableService {
private final GenTableMapper tableMapper;
private final GenTableColumnMapper genTableColumnMapper;
private static final String[] TABLE_IGNORE = new String[]{"sj_", "flow_", "gen_"};
private static final String[] TABLE_IGNORE = new String[]{"sai_", "sj_", "flow_", "gen_"};
/**
* 查询业务字段列表
@@ -46,7 +46,7 @@ public class GenUtils {
* @param table 所属业务表对象
*/
public static void initColumnField(GenTableColumn column, GenTable table) {
String dataType = getDbType(column.getColumnType()).toLowerCase();
String dataType = normalizeDbType(getDbType(column.getColumnType()));
// 统一转小写 避免有些数据库默认大写问题 如果需要特别书写方式 请在实体类增加注解标注别名
String columnName = column.getColumnName().toLowerCase();
Integer columnLength = getColumnLength(column.getColumnType());
@@ -58,6 +58,8 @@ public class GenUtils {
column.setJavaField(StringUtils.toCamelCase(columnName));
// 设置默认类型
column.setJavaType(GenConstants.TYPE_STRING);
// 设置默认HTML类型
column.setHtmlType(GenConstants.HTML_INPUT);
column.setQueryType(GenConstants.QUERY_EQ);
if (arraysContains(GenConstants.COLUMNTYPE_STR, dataType) || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType)) {
@@ -148,16 +150,20 @@ public class GenUtils {
if (isBooleanColumn(dataType, columnLength, columnScale, columnName)) {
return GenConstants.TYPE_BOOLEAN;
}
if (arraysContains(new String[]{"decimal", "numeric", "money", "smallmoney"}, dataType)) {
return columnScale > 0 ? GenConstants.TYPE_BIGDECIMAL : resolveIntegerJavaType(columnLength);
if (arraysContains(new String[]{"decimal", "dec", "fixed", "numeric", "money", "smallmoney"}, dataType)) {
return GenConstants.TYPE_BIGDECIMAL;
}
if (arraysContains(new String[]{"float", "float4", "float8", "double", "real", "double precision"}, dataType)) {
if (arraysContains(new String[]{"float4", "real", "binary_float"}, dataType)) {
return GenConstants.TYPE_FLOAT;
}
if (arraysContains(new String[]{"float", "float8", "double", "double precision", "binary_double"}, dataType)) {
return GenConstants.TYPE_DOUBLE;
}
if (arraysContains(new String[]{"bigint", "int8", "bigserial"}, dataType)) {
if (arraysContains(new String[]{"bigint", "int8", "bigserial", "serial8"}, dataType)) {
return GenConstants.TYPE_LONG;
}
if (arraysContains(new String[]{"smallint", "mediumint", "int", "int2", "int4", "integer", "smallserial", "serial"}, dataType)) {
if (arraysContains(new String[]{"tinyint", "smallint", "mediumint", "int", "int2", "int4", "integer",
"smallserial", "serial", "serial2", "serial4"}, dataType)) {
return GenConstants.TYPE_INTEGER;
}
if (StringUtils.equals(dataType, "number")) {
@@ -169,6 +175,19 @@ public class GenUtils {
return GenConstants.TYPE_LONG;
}
/**
* 规范化数据库字段类型去除 MySQL 等数据库的类型修饰
*
* @param dataType 数据库字段类型
* @return 规范化后的数据库字段类型
*/
private static String normalizeDbType(String dataType) {
return StringUtils.trim(dataType)
.toLowerCase()
.replace(" unsigned", "")
.replace(" zerofill", "");
}
/**
* 根据整数字段长度解析 Java 类型
*
@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.extra.template.TemplateConfig;
import cn.hutool.extra.template.TemplateEngine;
import cn.hutool.extra.template.TemplateUtil;
import cn.hutool.extra.template.engine.freemarker.FreemarkerEngine;
@@ -12,12 +13,10 @@ import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.DateUtils;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.mybatis.enums.DataBaseType;
import org.dromara.common.mybatis.helper.DataBaseHelper;
import org.dromara.gen.config.properties.GenProperties;
import org.dromara.gen.constant.GenConstants;
import org.dromara.gen.domain.GenTable;
import org.dromara.gen.domain.GenTableColumn;
@@ -27,6 +26,7 @@ import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.Consumer;
@@ -59,9 +59,13 @@ public class TemplateEngineUtils {
static {
// 模板引擎初始化
GenProperties properties = SpringUtils.getBean(GenProperties.class);
properties.getTemplateConfig().setCustomEngine(FreemarkerEngine.class);
TEMPLATE_ENGINE = TemplateUtil.createEngine(properties.getTemplateConfig());
TemplateConfig templateConfig = new TemplateConfig(StandardCharsets.UTF_8, StringUtils.EMPTY, TemplateConfig.ResourceMode.CLASSPATH);
templateConfig.setCustomEngine(FreemarkerEngine.class);
TEMPLATE_ENGINE = TemplateUtil.createEngine(templateConfig);
// 禁用数字千分位格式化 避免 Long 类型的 ID 渲染是被加上逗号
if (TEMPLATE_ENGINE instanceof FreemarkerEngine freemarkerEngine) {
freemarkerEngine.getConfiguration().setNumberFormat("computer");
}
TEMPLATE_MAPPER = PathNamedTemplate.form(TEMPLATE_ENGINE, GenConstants.TEMPLATE_PATHS);
}
@@ -168,6 +172,7 @@ public class TemplateEngineUtils {
boolean needDigit = false;
boolean needDateField = false;
boolean needSwitchField = false;
boolean needParseTime = false;
String firstTreeListField = StringUtils.EMPTY;
for (GenTableColumn column : genTable.getColumns()) {
boolean writable = column.isInsert() || column.isEdit();
@@ -183,6 +188,7 @@ public class TemplateEngineUtils {
needDigit = needDigit || writable && StringUtils.equals(column.getHtmlType(), GenConstants.HTML_INPUT_NUMBER);
needDateField = needDateField || writable && StringUtils.equals(column.getHtmlType(), GenConstants.HTML_DATETIME);
needSwitchField = needSwitchField || (writable || column.isList()) && StringUtils.equals(column.getHtmlType(), GenConstants.HTML_SWITCH);
needParseTime = needParseTime || column.isList() && StringUtils.equals(column.getHtmlType(), GenConstants.HTML_DATETIME);
if (StringUtils.isBlank(firstTreeListField) && column.isList()) {
firstTreeListField = column.getJavaField();
}
@@ -202,6 +208,7 @@ public class TemplateEngineUtils {
context.put("needDigit", needDigit);
context.put("needDateField", needDateField);
context.put("needSwitchField", needSwitchField);
context.put("needParseTime", needParseTime);
context.put("firstTreeListField", firstTreeListField);
}
@@ -351,6 +358,7 @@ public class TemplateEngineUtils {
String javaPath = PROJECT_PATH + "/" + StringUtils.replace(packageName, ".", "/");
String mybatisPath = MYBATIS_PATH + "/" + moduleName;
String frontendPath = getFrontendPath(genTable.getFrontendType());
String frontendPagePath = getFrontendPagePath(genTable.getFrontendType());
// templatePath
// genFilePathFormat
if (template.contains("domain.java.")) {
@@ -376,10 +384,10 @@ public class TemplateEngineUtils {
} else if (template.contains("types.ts.")) {
fileName = StringUtils.format("{}/api/{}/{}/types.ts", frontendPath, moduleName, businessName);
} else if (isFrontendPageTemplate(template, GenConstants.FRONTEND_INDEX_TEMPLATE_PREFIX)) {
fileName = StringUtils.format("{}/views/{}/{}/index.{}", frontendPath, moduleName, businessName,
fileName = StringUtils.format("{}/{}/{}/{}/index.{}", frontendPath, frontendPagePath, moduleName, businessName,
getFrontendPageExtension(template, GenConstants.FRONTEND_INDEX_TEMPLATE_PREFIX));
} else if (isFrontendPageTemplate(template, GenConstants.FRONTEND_INDEX_TREE_TEMPLATE_PREFIX)) {
fileName = StringUtils.format("{}/views/{}/{}/index.{}", frontendPath, moduleName, businessName,
fileName = StringUtils.format("{}/{}/{}/{}/index.{}", frontendPath, frontendPagePath, moduleName, businessName,
getFrontendPageExtension(template, GenConstants.FRONTEND_INDEX_TREE_TEMPLATE_PREFIX));
}
return fileName;
@@ -395,6 +403,16 @@ public class TemplateEngineUtils {
return resolveFrontendType(frontendType);
}
/**
* 获取前端页面输出目录
*
* @param frontendType 前端模板类型
* @return 页面输出目录
*/
private static String getFrontendPagePath(String frontendType) {
return GenConstants.FRONTEND_TYPE_REACT.equals(resolveFrontendType(frontendType)) ? "pages" : "views";
}
/**
* 获取前端页面文件扩展名
*
@@ -507,16 +525,26 @@ public class TemplateEngineUtils {
List<GenTableColumn> columns = genTable.getColumns();
HashSet<String> importList = new HashSet<>();
for (GenTableColumn column : columns) {
if (!column.isSuperColumn() && GenConstants.TYPE_DATE.equals(column.getJavaType())) {
if (needImportColumn(column) && GenConstants.TYPE_DATE.equals(column.getJavaType())) {
importList.add("java.time.LocalDateTime");
importList.add("com.fasterxml.jackson.annotation.JsonFormat");
} else if (!column.isSuperColumn() && GenConstants.TYPE_BIGDECIMAL.equals(column.getJavaType())) {
} else if (needImportColumn(column) && GenConstants.TYPE_BIGDECIMAL.equals(column.getJavaType())) {
importList.add("java.math.BigDecimal");
}
}
return importList;
}
/**
* 判断列是否需要生成类型导入
*
* @param column 列对象
* @return 需要生成类型导入返回 {@code true}
*/
private static boolean needImportColumn(GenTableColumn column) {
return !column.isSuperColumn() || column.isQuery();
}
/**
* 根据列类型获取字典组
*
@@ -700,7 +728,16 @@ public class TemplateEngineUtils {
if (StringUtils.equals(column.getJavaType(), GenConstants.TYPE_LONG)) {
return value + "L";
}
if (StringUtils.equalsAny(column.getJavaType(), GenConstants.TYPE_INTEGER, GenConstants.TYPE_DOUBLE, GenConstants.TYPE_BIGDECIMAL)) {
if (StringUtils.equals(column.getJavaType(), GenConstants.TYPE_FLOAT)) {
return value + "F";
}
if (StringUtils.equals(column.getJavaType(), GenConstants.TYPE_DOUBLE)) {
return value + "D";
}
if (StringUtils.equals(column.getJavaType(), GenConstants.TYPE_BIGDECIMAL)) {
return "new java.math.BigDecimal(\"" + value + "\")";
}
if (StringUtils.equals(column.getJavaType(), GenConstants.TYPE_INTEGER)) {
return value;
}
return "\"" + value + "\"";
@@ -717,7 +754,8 @@ public class TemplateEngineUtils {
if (ObjectUtil.isNull(column) || StringUtils.isBlank(value)) {
return "undefined";
}
if (StringUtils.equalsAny(column.getJavaType(), GenConstants.TYPE_LONG, GenConstants.TYPE_INTEGER, GenConstants.TYPE_DOUBLE, GenConstants.TYPE_BIGDECIMAL)) {
if (StringUtils.equalsAny(column.getJavaType(), GenConstants.TYPE_LONG, GenConstants.TYPE_INTEGER,
GenConstants.TYPE_DOUBLE, GenConstants.TYPE_FLOAT, GenConstants.TYPE_BIGDECIMAL)) {
return value;
}
return "'" + value + "'";
@@ -30,7 +30,7 @@ public class ${ClassName}Bo implements Serializable {
private static final long serialVersionUID = 1L;
<#list columns as column>
<#if !table.isSuperColumn(column.javaField) && (column.query || column.insert || column.edit)>
<#if column.query || (!table.isSuperColumn(column.javaField) && (column.insert || column.edit))>
/**
* ${column.columnComment}
*/
@@ -41,7 +41,7 @@ public class ${ClassName}Bo implements Serializable {
<#elseif column.edit>
<#assign Group = "EditGroup.class">
</#if>
<#if column.required>
<#if column.required && !table.isSuperColumn(column.javaField) && (column.insert || column.edit)>
<#if column.javaType == 'String'>
@NotBlank(message = "${column.columnComment}不能为空", groups = { ${Group} })
<#else>
@@ -60,4 +60,3 @@ public class ${ClassName}Bo implements Serializable {
</#if>
}
@@ -44,7 +44,7 @@ public class ${ClassName}Vo implements Serializable {
@ExcelDictFormat(dictType = "${column.dictType}")
<#elseif parentheseIndex != -1>
@ExcelProperty(value = "${column.columnLabel}", converter = ExcelDictConvert.class)
@ExcelDictFormat(readConverterExp = "${column.readConverterExp}()")
@ExcelDictFormat(readConverterExp = "${column.readConverterExp()}")
<#else>
@ExcelProperty(value = "${column.columnLabel}")
</#if>
@@ -92,7 +92,3 @@ export function del${BusinessName}(${pkColumn.javaField}: string | number | Arra
method: 'delete'
});
}
@@ -62,6 +62,7 @@ import { useDict } from '@/hooks/useDict';
<#if enableExport>
import { useTableExport } from '@/hooks/useTableExport';
</#if>
import { useTableScroll } from '@/hooks/useTableScroll';
import { useTreeTableExpand } from '@/hooks/useTreeTableExpand';
import { useUserStore } from '@/stores/userStore';
<#if needDict>
@@ -103,6 +104,7 @@ const ${statusField}InactiveValue = <#if statusColumn.javaType == "Boolean">fals
</#if>
export default function ${BusinessName}Page() {
const actionRef = useRef<ActionType | undefined>(undefined);
const { tableScroll } = useTableScroll(1000);
const [form] = Form.useForm<${BusinessName}Form>();
const userInfo = useUserStore(state => state.userInfo);
<#if needDict>
@@ -308,7 +310,7 @@ export default function ${BusinessName}Page() {
actionRef={actionRef}
rowKey="${treeCode}"
columns={columns}
scroll={{ x: 1000 }}
scroll={tableScroll}
pagination={false}
search={{ labelWidth: 90 }}
expandable={{
@@ -365,8 +367,7 @@ export default function ${BusinessName}Page() {
<ProFormText name="${pkColumn.javaField}" hidden />
<#list columns as column>
<#if (column.insert || column.edit) && !column.pk>
<#assign field = column.javaField>
<#if field == treeParentCode>
<#if column.javaField == treeParentCode>
<ProFormTreeSelect
name="${column.javaField}"
label="${column.columnLabel}"
@@ -421,7 +422,3 @@ export default function ${BusinessName}Page() {
</PageContainer>
);
}
@@ -64,6 +64,7 @@ import { useDateRangeQuery } from '@/hooks/useDateRangeQuery';
<#if enableExport>
import { useTableExport } from '@/hooks/useTableExport';
</#if>
import { useTableScroll } from '@/hooks/useTableScroll';
import { useTableSelection } from '@/hooks/useTableSelection';
import { useUserStore } from '@/stores/userStore';
<#if needDict>
@@ -90,6 +91,7 @@ const ${statusField}InactiveValue = <#if statusColumn.javaType == "Boolean">fals
</#if>
export default function ${BusinessName}Page() {
const actionRef = useRef<ActionType | undefined>(undefined);
const { tableScroll } = useTableScroll(1000);
const [form] = Form.useForm<${BusinessName}Form>();
const userInfo = useUserStore(state => state.userInfo);
<#if needDict>
@@ -292,7 +294,7 @@ const { applyDateRange: apply${column.capJavaField}DateRange } = useDateRangeQue
actionRef={actionRef}
rowKey="${pkColumn.javaField}"
columns={columns}
scroll={{ x: 1000 }}
scroll={tableScroll}
search={{ labelWidth: 90 }}
pagination={{ defaultPageSize: 10, showSizeChanger: true }}
rowSelection={{ selectedRowKeys: ids, onChange: handleSelectionChange }}
@@ -359,7 +361,6 @@ query = apply${column.capJavaField}DateRange(query, params.${column.javaField}Ra
<ProFormText name="${pkColumn.javaField}" hidden />
<#list columns as column>
<#if (column.insert || column.edit) && !column.pk>
<#assign field = column.javaField>
<#if column.htmlType == "input">
<ProFormText name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
<#elseif column.htmlType == "textarea">
@@ -403,7 +404,3 @@ query = apply${column.capJavaField}DateRange(query, params.${column.javaField}Ra
</PageContainer>
);
}
@@ -52,6 +52,3 @@ export interface ${BusinessName}Query<#if !table.tree> extends PageQuery</#if> {
*/
params?: Record<string, unknown>;
}
@@ -1,5 +1,7 @@
import type { ${BusinessName}Form, ${BusinessName}Query, ${BusinessName}VO } from '@/api/${moduleName}/${businessName}/types';
<#if !table.tree>
import type { PageResult } from '@/api/types';
</#if>
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
@@ -9,7 +11,7 @@ import request from '@/utils/request';
* @returns {*}
*/
export const list${BusinessName} = (query?: ${BusinessName}Query): AxiosPromise<PageResult<${BusinessName}VO>> => {
export const list${BusinessName} = (query?: ${BusinessName}Query): AxiosPromise<<#if table.tree>${BusinessName}VO[]<#else>PageResult<${BusinessName}VO></#if>> => {
return request({
url: '/${moduleName}/${businessName}/list',
method: 'get',
@@ -100,4 +102,3 @@ export const del${BusinessName} = (${pkColumn.javaField}: string | number | Arra
};
@@ -45,7 +45,7 @@
</#if>
</el-select>
</el-form-item>
<#elseif (column.htmlType == "select" || column.htmlType == "radio") && column.dictType?has_content>
<#elseif (column.htmlType == "select" || column.htmlType == "radio") && !(column.dictType?has_content)>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${column.columnLabel}" clearable>
<el-option label="请选择字典生成" value="" />
@@ -312,7 +312,7 @@
></el-option>
</el-select>
</el-form-item>
<#elseif column.htmlType == "select" && column.dictType?has_content>
<#elseif column.htmlType == "select" && !(column.dictType?has_content)>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="form.${column.javaField}" placeholder="请选择${column.columnLabel}">
<el-option label="请选择字典生成" value="" />
@@ -329,7 +329,7 @@
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<#elseif column.htmlType == "checkbox" && column.dictType?has_content>
<#elseif column.htmlType == "checkbox" && !(column.dictType?has_content)>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-checkbox-group v-model="form.${column.javaField}">
<el-checkbox>请选择字典生成</el-checkbox>
@@ -349,7 +349,7 @@
>{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
<#elseif column.htmlType == "radio" && column.dictType?has_content>
<#elseif column.htmlType == "radio" && !(column.dictType?has_content)>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-radio-group v-model="form.${column.javaField}">
<el-radio value="1">请选择字典生成</el-radio>
@@ -689,4 +689,3 @@ onMounted(() => {
getList();
});
</script>
@@ -45,7 +45,7 @@
</#if>
</el-select>
</el-form-item>
<#elseif (column.htmlType == "select" || column.htmlType == "radio") && column.dictType?has_content>
<#elseif (column.htmlType == "select" || column.htmlType == "radio") && !(column.dictType?has_content)>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${column.columnLabel}" clearable >
<el-option label="请选择字典生成" value="" />
@@ -60,7 +60,7 @@
placeholder="请选择${column.columnLabel}"
/>
</el-form-item>
<#elseif column.htmlType == "datetime" && column.queryType == "BETWEEN">
<#elseif column.htmlType == "datetime" && column.queryType == "BETWEEN" && column.query>
<el-form-item label="${column.columnLabel}" style="width: 308px">
<el-date-picker
v-model="dateRange${column.capJavaField}"
@@ -233,7 +233,8 @@
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="${businessName}FormRef" :model="form" :rules="rules" label-width="80px">
<#list columns as column>
<#if (column.insert || column.edit) && !column.pk>`n<#if column.htmlType == "input">
<#if (column.insert || column.edit) && !column.pk>
<#if column.htmlType == "input">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-input v-model="form.${column.javaField}" placeholder="请输入${column.columnLabel}" />
</el-form-item>
@@ -268,7 +269,7 @@
></el-option>
</el-select>
</el-form-item>
<#elseif column.htmlType == "select" && column.dictType?has_content>
<#elseif column.htmlType == "select" && !(column.dictType?has_content)>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="form.${column.javaField}" placeholder="请选择${column.columnLabel}">
<el-option label="请选择字典生成" value="" />
@@ -285,7 +286,7 @@
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<#elseif column.htmlType == "checkbox" && column.dictType?has_content>
<#elseif column.htmlType == "checkbox" && !(column.dictType?has_content)>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-checkbox-group v-model="form.${column.javaField}">
<el-checkbox>请选择字典生成</el-checkbox>
@@ -305,7 +306,7 @@
>{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
<#elseif column.htmlType == "radio" && column.dictType?has_content>
<#elseif column.htmlType == "radio" && !(column.dictType?has_content)>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-radio-group v-model="form.${column.javaField}">
<el-radio value="1">请选择字典生成</el-radio>
@@ -380,6 +381,9 @@ import { useTableSelection } from '@/hooks/table/useTableSelection';
<#if needDict>
import { useDict } from '@/utils/dict';
</#if>
<#if needParseTime>
import { parseTime } from '@/utils/ruoyi';
</#if>
import modal from '@/plugins/modal';
<#if enableExport>
import { download as requestDownload } from '@/utils/request';
@@ -400,7 +404,7 @@ const { loading, withLoading } = useLoading(true);
const { showSearch } = useSearchToggle();
const total = ref(0);
<#list columns as column>
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN">
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN" && column.query>
const {
dateRange: dateRange${column.capJavaField},
applyDateRange: apply${column.capJavaField}DateRange,
@@ -472,7 +476,7 @@ const getList = async () => {
<#if needAddDateRange>
let params = queryParams.value;
<#list columns as column>
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN">
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN" && column.query>
params = apply${column.capJavaField}DateRange(params);
</#if>
</#list>
@@ -505,7 +509,7 @@ const { resetQuery } = useSearchReset({
initialPageSize: 10,
resetExtras: () => {
<#list columns as column>
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN">
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN" && column.query>
reset${column.capJavaField}DateRange();
</#if>
</#list>
@@ -565,8 +569,8 @@ const handleDelete = async (row?: Partial<${BusinessName}VO>) => {
await getList();
};
/** 导出按钮操作 */
<#if enableExport>
/** 导出按钮操作 */
const handleExport = () => {
requestDownload(
'${moduleName}/${businessName}/export',
@@ -1,5 +1,3 @@
import type { BaseEntity<#if !table.tree>, PageQuery</#if> } from '@/api/types';
export interface ${BusinessName}VO {
<#list columns as column>
<#if column.list>
@@ -8,9 +8,3 @@ gen:
autoRemovePre: false
# 表前缀(生成类名不会包含表前缀,多个用逗号分隔)
tablePrefix: sys_
# 模板配置
templateConfig:
# 模版加载模式
resourceMode: CLASSPATH
# 模板加载路径
# path: fm
@@ -60,11 +60,16 @@ public class ProfileUserVo implements Serializable {
*/
private String gender;
/**
* 头像 OSS ID
*/
private Long avatar;
/**
* 头像地址
*/
@Translation(type = TransConstant.OSS_ID_TO_URL)
private Long avatar;
@Translation(type = TransConstant.OSS_ID_TO_URL, mapper = "avatar")
private String avatarUrl;
/**
* 最后登录IP
@@ -70,11 +70,16 @@ public class SysUserVo implements Serializable {
*/
private String gender;
/**
* 头像 OSS ID
*/
private Long avatar;
/**
* 头像地址
*/
@Translation(type = TransConstant.OSS_ID_TO_URL)
private Long avatar;
@Translation(type = TransConstant.OSS_ID_TO_URL, mapper = "avatar")
private String avatarUrl;
/**
* 密码
@@ -219,9 +219,9 @@ public class SysClientServiceImpl implements ISysClientService {
* @return 逗号拼接后的规则串
*/
private String resolveRuleValue(String rawValue, List<String> listValue, UnaryOperator<String> normalizer) {
List<String> rules = CollUtil.isNotEmpty(listValue)
? listValue
: StringUtils.str2List(rawValue, CLIENT_RULE_SEPARATOR_REGEX, true, true);
List<String> rules = rawValue != null
? StringUtils.str2List(rawValue, CLIENT_RULE_SEPARATOR_REGEX, true, true)
: listValue;
if (CollUtil.isEmpty(rules)) {
return listValue != null || rawValue != null ? "" : null;
}
@@ -123,8 +123,11 @@ public class SysMenuServiceImpl implements ISysMenuService {
} else {
menus = menuMapper.selectMenuTreeByUserId(userId);
}
if (CollUtil.isEmpty(menus)) {
return CollUtil.newArrayList();
}
return TreeBuildUtils.build(menus, Constants.TOP_PARENT_ID, SysMenu::getParentId, (menu, nodeTreeMaps) -> {
List<SysMenu> menuTree = TreeBuildUtils.build(menus, Constants.TOP_PARENT_ID, SysMenu::getParentId, (menu, nodeTreeMaps) -> {
// 将当前节点的菜单ID用作父节点ID
Long menuParentId = menu.getMenuId();
// 从动态规划表中取出子节点列表
@@ -134,6 +137,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
// 如果存在根节点指向尾节点的情况则会出现环形依赖但在菜单表中基本不会出现这种情况...
menu.setChildren(childMenus);
});
return CollUtil.isEmpty(menuTree) ? CollUtil.newArrayList() : menuTree;
}
/**
@@ -157,6 +161,9 @@ public class SysMenuServiceImpl implements ISysMenuService {
*/
@Override
public List<RouterVo> buildMenus(List<SysMenu> menus) {
if (CollUtil.isEmpty(menus)) {
return CollUtil.newArrayList();
}
List<RouterVo> routers = new LinkedList<>();
for (SysMenu menu : menus) {
String name = menu.getRouteName() + menu.getMenuId();
+6
View File
@@ -96,6 +96,12 @@
<artifactId>warm-flow-plugin-ui-sb-web</artifactId>
</dependency>
<!-- LiteFlow 规则编排 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-liteflow</artifactId>
</dependency>
</dependencies>
</project>
@@ -108,13 +108,4 @@ public interface FlowConstant {
*/
String VAR_IGNORE_COOPERATE = "ignoreCooperate";
/**
* 未删除正常数据
*/
Integer NOT_DELETED = 0;
/**
* 已删除逻辑删除
*/
Integer DELETED = 1;
}
@@ -63,7 +63,6 @@ public class FlwSpelController extends BaseController {
}
/**
* /**
* 新增流程 SpEL 表达式定义
*
* @param bo 表达式信息
@@ -1,5 +1,6 @@
package org.dromara.workflow.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.PageResult;
import org.dromara.common.core.domain.R;
@@ -89,13 +90,13 @@ public class FlwTaskController extends BaseController {
}
/**
* /**
* 查询全部待办任务
*
* @param flowTaskBo 参数
* @param pageQuery 分页
* @return 待办任务分页数据
*/
@SaCheckPermission("workflow:task:list")
@GetMapping("/pageByAllTaskWait")
public R<PageResult<FlowTaskVo>> pageByAllTaskWait(FlowTaskBo flowTaskBo, PageQuery pageQuery) {
return R.ok(flwTaskService.pageByAllTaskWait(flowTaskBo, pageQuery));
@@ -108,6 +109,7 @@ public class FlwTaskController extends BaseController {
* @param pageQuery 分页
* @return 已办任务分页数据
*/
@SaCheckPermission("workflow:task:list")
@GetMapping("/pageByAllTaskFinish")
public R<PageResult<FlowHisTaskVo>> pageByAllTaskFinish(FlowTaskBo flowTaskBo, PageQuery pageQuery) {
return R.ok(flwTaskService.pageByAllTaskFinish(flowTaskBo, pageQuery));
@@ -126,7 +128,6 @@ public class FlwTaskController extends BaseController {
}
/**
* /**
* 根据任务 id 查询任务详情
*
* @param taskId 任务id
@@ -138,7 +139,6 @@ public class FlwTaskController extends BaseController {
}
/**
* /**
* 获取流程下一节点信息
*
* @param bo 参数
@@ -150,7 +150,6 @@ public class FlwTaskController extends BaseController {
}
/**
* /**
* 终止流程任务
*
* @param bo 参数
@@ -186,6 +185,7 @@ public class FlwTaskController extends BaseController {
*/
@Log(title = "任务管理", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@SaCheckPermission("workflow:task:edit")
@PutMapping("/updateAssignee/{userId}")
public R<Void> updateAssignee(@RequestBody List<Long> taskIdList, @PathVariable String userId) {
return toAjax(flwTaskService.updateAssignee(taskIdList, userId));
@@ -235,6 +235,7 @@ public class FlwTaskController extends BaseController {
*/
@PostMapping("/urgeTask")
@Log(title = "任务管理", businessType = BusinessType.INSERT)
@SaCheckPermission("workflow:task:edit")
public R<Void> urgeTask(@RequestBody FlowUrgeTaskBo bo) {
return toAjax(flwTaskService.urgeTask(bo));
}
@@ -53,7 +53,6 @@ public class TestLeaveController extends BaseController {
}
/**
* /**
* 导出请假列表
*
* @param bo 查询条件
@@ -0,0 +1,44 @@
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;
/**
* 办理任务 LiteFlow 上下文
* <p>
* 链路内各节点通过该对象传递任务实例办理参数等中间结果避免组件之间重复查询
*
* @author may
*/
@Data
public class CompleteTaskContext {
/**
* 办理任务入参
*/
private final CompleteTaskBo completeTaskBo;
/**
* 当前待办理的运行任务由校验节点加载
*/
private FlowTask flowTask;
/**
* 当前任务所属流程实例由校验节点加载
*/
private Instance instance;
/**
* Warm-Flow 办理参数由参数构建节点生成并供执行自动审批节点复用
*/
private FlowParams flowParams;
/**
* 流程实例是否开启自动审批由参数构建节点从实例变量中解析
*/
private Boolean autoPass;
}
@@ -0,0 +1,59 @@
package org.dromara.workflow.domain.context;
import lombok.Data;
import org.dromara.warm.flow.orm.entity.FlowInstance;
import java.util.Collection;
import java.util.List;
/**
* 删除流程实例 LiteFlow 上下文
*
* @author may
*/
@Data
public class InstanceDeleteContext {
/**
* 按业务 id 删除时的业务 id 集合
*/
private final List<String> businessIds;
/**
* 按实例 id 删除时的实例 id 集合
*/
private final Collection<Long> instanceIds;
/**
* 是否删除历史实例及其历史任务数据
*/
private final boolean history;
/**
* 待删除流程实例
*/
private List<FlowInstance> flowInstances;
/**
* 实际执行删除的实例 id
*/
private List<Long> deleteInstanceIds;
/**
* 删除结果
*/
private boolean result;
public static InstanceDeleteContext byBusinessIds(List<String> businessIds) {
return new InstanceDeleteContext(businessIds, null, false);
}
public static InstanceDeleteContext byInstanceIds(Collection<Long> instanceIds) {
return new InstanceDeleteContext(null, instanceIds, false);
}
public static InstanceDeleteContext byHistoryInstanceIds(Collection<Long> instanceIds) {
return new InstanceDeleteContext(null, instanceIds, true);
}
}
@@ -0,0 +1,70 @@
package org.dromara.workflow.domain.context;
import lombok.Data;
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.orm.entity.FlowInstance;
import org.dromara.workflow.api.domain.StartProcessReturnDTO;
import org.dromara.workflow.domain.FlowInstanceBizExt;
import org.dromara.workflow.domain.bo.StartProcessBo;
import java.util.List;
import java.util.Map;
/**
* 启动流程 LiteFlow 上下文
* <p>
* 链路内保存启动参数流程定义流程实例和首个任务等中间结果
*
* @author may
*/
@Data
public class StartProcessContext {
/**
* 启动流程入参
*/
private final StartProcessBo startProcessBo;
/**
* 业务唯一标识
*/
private String businessId;
/**
* 流程变量
*/
private Map<String, Object> variables;
/**
* 流程实例业务扩展信息
*/
private FlowInstanceBizExt bizExt;
/**
* 已存在的流程实例非空时走续提交逻辑
*/
private FlowInstance existingInstance;
/**
* 已发布的流程定义
*/
private Definition definition;
/**
* 新启动的流程实例
*/
private Instance instance;
/**
* 当前流程实例下生成的任务列表
*/
private List<Task> taskList;
/**
* 启动流程返回结果
*/
private StartProcessReturnDTO startProcessReturn;
}
@@ -0,0 +1,55 @@
package org.dromara.workflow.domain.context;
import lombok.Data;
import org.dromara.warm.flow.core.dto.FlowParams;
import org.dromara.warm.flow.core.entity.Task;
import org.dromara.warm.flow.orm.entity.FlowNode;
import org.dromara.workflow.common.enums.TaskOperationEnum;
import org.dromara.workflow.domain.bo.TaskOperationBo;
/**
* 任务操作 LiteFlow 上下文
* <p>
* 用于在委派转办加签减签链路中传递操作类型任务节点和办理参数
*
* @author may
*/
@Data
public class TaskOperationContext {
/**
* 任务操作入参
*/
private final TaskOperationBo taskOperationBo;
/**
* 操作类型编码
*/
private final String taskOperation;
/**
* 解析后的操作枚举
*/
private TaskOperationEnum operation;
/**
* Warm-Flow 操作参数
*/
private FlowParams flowParams;
/**
* 当前任务
*/
private Task task;
/**
* 当前任务对应的流程节点
*/
private FlowNode flowNode;
/**
* 操作执行结果
*/
private boolean result;
}
@@ -0,0 +1,33 @@
package org.dromara.workflow.handler;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.R;
import org.dromara.warm.flow.core.exception.FlowException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 工作流失败异常
*
* @author Lion Li
*/
@Slf4j
@RestControllerAdvice
public class FlowExceptionHandler {
/**
* 工作流失败异常
*
* @param e 异常信息
* @param request 当前请求
* @return 统一失败响应
*/
@ExceptionHandler(FlowException.class)
public R<Void> handleFlowException(FlowException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}'", requestURI, e);
return R.fail(e.getMessage());
}
}
@@ -0,0 +1,95 @@
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.*;
/**
* 执行当前处理人的后续自动审批
* <p>
* 仅当流程实例开启自动审批时生效用于连续跳过同一登录人负责的后续待办任务
*
* @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<FlowTask> flowTaskList = selectByInstId(instanceId);
if (CollUtil.isEmpty(flowTaskList)) {
return;
}
List<User> 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<User> 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<FlowTask> selectByInstId(Long instanceId) {
return flowTaskMapper.selectList(QueryBuilder.lambda(FlowTask.class)
.eq(FlowTask::getInstanceId, instanceId)
.build());
}
}
@@ -0,0 +1,54 @@
package org.dromara.workflow.liteflow.complete;
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.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.enums.TaskStatusEnum;
import org.dromara.workflow.domain.bo.CompleteTaskBo;
import org.dromara.workflow.domain.context.CompleteTaskContext;
import java.util.Map;
import static org.dromara.workflow.common.constant.FlowConstant.*;
/**
* 构建办理参数并执行当前任务跳转
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@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<String, Object> 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);
}
}
@@ -0,0 +1,23 @@
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());
}
}
@@ -0,0 +1,100 @@
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.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.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;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.dromara.workflow.common.constant.FlowConstant.*;
/**
* 准备任务办理上下文
* <p>
* 负责加载任务实例补齐流程变量并解析弹窗选择的后续办理人
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("completePrepare")
public class CompletePrepareComponent extends NodeComponent {
private final FlowTaskMapper flowTaskMapper;
private final InsService insService;
@Override
public void process() {
CompleteTaskContext context = getContextBean(CompleteTaskContext.class);
CompleteTaskBo completeTaskBo = context.getCompleteTaskBo();
FlowTask flowTask = flowTaskMapper.selectById(completeTaskBo.getTaskId());
if (ObjectUtil.isNull(flowTask)) {
throw new ServiceException("流程任务不存在或任务已审批!");
}
Instance instance = insService.getById(flowTask.getInstanceId());
if (ObjectUtil.isNull(instance)) {
throw new ServiceException("流程实例不存在");
}
context.setFlowTask(flowTask);
context.setInstance(instance);
Map<String, Object> variables = completeTaskBo.getVariables();
variables.put(FLOW_COPY_LIST, completeTaskBo.getFlowCopyList());
variables.put(MESSAGE_TYPE, completeTaskBo.getMessageType());
variables.put(MESSAGE_NOTICE, completeTaskBo.getNotice());
if (BusinessStatusEnum.isDraftOrCancelOrBack(instance.getFlowStatus())) {
// 草稿撤销驳回状态再次办理时等同于重新提交条件表达式通过 SUBMIT 识别提交分支
variables.put(SUBMIT, true);
}
Map<String, Object> assigneeMap = setPopAssigneeMap(completeTaskBo.getAssigneeMap(), instance.getVariableMap());
if (CollUtil.isNotEmpty(assigneeMap)) {
variables.putAll(assigneeMap);
}
}
/**
* 将弹窗处理人映射成通过/退回状态下的办理人变量
*/
private Map<String, Object> setPopAssigneeMap(Map<String, Object> assigneeMap, Map<String, Object> variablesMap) {
Map<String, Object> map = new HashMap<>();
if (CollUtil.isEmpty(assigneeMap)) {
return map;
}
for (Map.Entry<String, Object> entry : assigneeMap.entrySet()) {
if (variablesMap.containsKey(entry.getKey())) {
String userIds = variablesMap.get(entry.getKey()).toString();
if (StringUtils.isNotBlank(userIds)) {
Set<String> hashSet = CollUtil.newHashSet();
List<String> popUserIds = StringUtils.str2List(entry.getValue().toString(), StringUtils.SEPARATOR, true, true);
List<String> variableUserIds = StringUtils.str2List(userIds, StringUtils.SEPARATOR, true, true);
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));
}
} else {
map.put(TaskStatusEnum.PASS.getStatus() + StringUtils.COLON + entry.getKey(), entry.getValue());
map.put(TaskStatusEnum.BACK.getStatus() + StringUtils.COLON + entry.getKey(), entry.getValue());
}
}
return map;
}
}
@@ -0,0 +1,60 @@
package org.dromara.workflow.liteflow.instance;
import cn.hutool.core.util.ObjectUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.warm.flow.core.entity.Definition;
import org.dromara.warm.flow.core.entity.Instance;
import org.dromara.warm.flow.core.service.DefService;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.context.InstanceDeleteContext;
import org.dromara.workflow.handler.FlowProcessEventHandler;
import java.util.Map;
import java.util.function.Function;
/**
* 校验实例删除权限并发布业务删除事件
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@Slf4j
@LiteflowComponent("instanceDeleteEvent")
public class InstanceDeleteEventComponent extends NodeComponent {
private final DefService defService;
private final FlowProcessEventHandler flowProcessEventHandler;
@Override
public void process() {
InstanceDeleteContext context = getContextBean(InstanceDeleteContext.class);
String userId = LoginHelper.getUserIdStr();
context.getFlowInstances().forEach(flowInstance -> {
if (!LoginHelper.isSuperAdmin() && !flowInstance.getCreateBy().equals(userId)) {
throw new ServiceException("权限不足,无法删除流程实例信息!");
}
});
Map<Long, Definition> definitionMap = StreamUtils.toMap(
defService.getByIds(StreamUtils.toList(context.getFlowInstances(), Instance::getDefinitionId)),
Definition::getId,
Function.identity()
);
context.getFlowInstances().forEach(instance -> {
Definition definition = definitionMap.get(instance.getDefinitionId());
if (ObjectUtil.isNull(definition)) {
log.warn("实例 ID: {} 对应的流程定义信息未找到,跳过删除事件触发。", instance.getId());
return;
}
flowProcessEventHandler.processDeleteHandler(definition.getFlowCode(), instance.getBusinessId());
});
}
}
@@ -0,0 +1,79 @@
package org.dromara.workflow.liteflow.instance;
import cn.hutool.core.collection.CollUtil;
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.mybatis.core.query.QueryBuilder;
import org.dromara.warm.flow.core.FlowEngine;
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;
import java.util.Objects;
/**
* 执行流程实例删除
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("instanceDeleteExecute")
public class InstanceDeleteExecuteComponent extends NodeComponent {
private final InsService insService;
private final FlowHisTaskMapper flowHisTaskMapper;
private final FlowTaskMapper flowTaskMapper;
@Override
public void process() {
InstanceDeleteContext context = getContextBean(InstanceDeleteContext.class);
deleteCopyUsers(context.getDeleteInstanceIds());
if (!context.isHistory()) {
context.setResult(insService.remove(context.getDeleteInstanceIds()));
return;
}
List<FlowTask> flowTaskList = flowTaskMapper.selectList(QueryBuilder.lambda(FlowTask.class)
.in(FlowTask::getInstanceId, context.getDeleteInstanceIds())
.build());
if (CollUtil.isNotEmpty(flowTaskList)) {
FlowEngine.userService().deleteByTaskIds(StreamUtils.toList(flowTaskList, FlowTask::getId));
}
FlowEngine.taskService().deleteByInsIds(context.getDeleteInstanceIds());
FlowEngine.hisTaskService().deleteByInsIds(context.getDeleteInstanceIds());
FlowEngine.insService().removeByIds(context.getDeleteInstanceIds());
context.setResult(true);
}
/**
* 删除流程实例产生的抄送人员关联
*
* @param instanceIds 流程实例 ID 集合
*/
private void deleteCopyUsers(List<Long> instanceIds) {
List<FlowHisTask> copyTasks = flowHisTaskMapper.selectList(QueryBuilder.lambda(FlowHisTask.class)
.in(FlowHisTask::getInstanceId, instanceIds)
.eq(FlowHisTask::getFlowStatus, TaskStatusEnum.COPY.getStatus())
.build());
if (CollUtil.isEmpty(copyTasks)) {
return;
}
List<Long> copyTaskIds = copyTasks.stream()
.map(FlowHisTask::getTaskId)
.filter(Objects::nonNull)
.distinct()
.toList();
if (CollUtil.isNotEmpty(copyTaskIds)) {
FlowEngine.userService().deleteByTaskIds(copyTaskIds);
}
}
}
@@ -0,0 +1,24 @@
package org.dromara.workflow.liteflow.instance;
import cn.hutool.core.collection.CollUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeBooleanComponent;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.context.InstanceDeleteContext;
/**
* 判断删除流程实例时是否存在可执行删除的数据
*
* @author may
*/
@ConditionalOnEnable
@LiteflowComponent("instanceDeleteExists")
public class InstanceDeleteExistsComponent extends NodeBooleanComponent {
@Override
public boolean processBoolean() {
InstanceDeleteContext context = getContextBean(InstanceDeleteContext.class);
return CollUtil.isNotEmpty(context.getFlowInstances());
}
}
@@ -0,0 +1,52 @@
package org.dromara.workflow.liteflow.instance;
import cn.hutool.core.collection.CollUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.mybatis.core.query.QueryBuilder;
import org.dromara.warm.flow.orm.entity.FlowInstance;
import org.dromara.warm.flow.orm.mapper.FlowInstanceMapper;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.context.InstanceDeleteContext;
import java.util.ArrayList;
import java.util.List;
/**
* 加载待删除流程实例
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@Slf4j
@LiteflowComponent("instanceDeleteLoad")
public class InstanceDeleteLoadComponent extends NodeComponent {
private final FlowInstanceMapper flowInstanceMapper;
@Override
public void process() {
InstanceDeleteContext context = getContextBean(InstanceDeleteContext.class);
List<FlowInstance> flowInstances;
if (CollUtil.isNotEmpty(context.getBusinessIds())) {
flowInstances = flowInstanceMapper.selectList(QueryBuilder.lambda(FlowInstance.class)
.in(FlowInstance::getBusinessId, context.getBusinessIds())
.build());
context.setDeleteInstanceIds(StreamUtils.toList(flowInstances, FlowInstance::getId));
} else {
flowInstances = flowInstanceMapper.selectByIds(context.getInstanceIds());
context.setDeleteInstanceIds(new ArrayList<>(context.getInstanceIds()));
}
if (CollUtil.isEmpty(flowInstances)) {
log.warn("未找到对应的流程实例信息,无法执行删除操作。");
context.setResult(false);
return;
}
context.setFlowInstances(flowInstances);
}
}
@@ -0,0 +1,49 @@
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.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.enums.TaskStatusEnum;
import org.dromara.workflow.domain.context.TaskOperationContext;
/**
* 执行委派转办加签或减签
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@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;
switch (context.getOperation()) {
case DELEGATE_TASK -> {
context.getFlowParams().hisStatus(TaskStatusEnum.DEPUTE.getStatus());
result = taskService.depute(taskId, context.getFlowParams());
}
case TRANSFER_TASK -> {
context.getFlowParams().hisStatus(TaskStatusEnum.TRANSFER.getStatus());
result = taskService.transfer(taskId, context.getFlowParams());
}
case ADD_SIGNATURE -> {
context.getFlowParams().hisStatus(TaskStatusEnum.SIGN.getStatus());
result = taskService.addSignature(taskId, context.getFlowParams());
}
case REDUCTION_SIGNATURE -> {
context.getFlowParams().hisStatus(TaskStatusEnum.SIGN_OFF.getStatus());
result = taskService.reductionSignature(taskId, context.getFlowParams());
}
}
context.setResult(result);
}
}
@@ -0,0 +1,53 @@
package org.dromara.workflow.liteflow.operation;
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.exception.ServiceException;
import org.dromara.common.mybatis.core.query.QueryBuilder;
import org.dromara.warm.flow.core.entity.Task;
import org.dromara.warm.flow.core.enums.CooperateType;
import org.dromara.warm.flow.core.service.TaskService;
import org.dromara.warm.flow.orm.entity.FlowNode;
import org.dromara.warm.flow.orm.mapper.FlowNodeMapper;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.enums.TaskOperationEnum;
import org.dromara.workflow.domain.context.TaskOperationContext;
/**
* 加载任务节点并校验操作约束
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("taskOpLoad")
public class TaskOpLoadComponent extends NodeComponent {
private final TaskService taskService;
private final FlowNodeMapper flowNodeMapper;
@Override
public void process() {
TaskOperationContext context = getContextBean(TaskOperationContext.class);
Task task = taskService.getById(context.getTaskOperationBo().getTaskId());
if (ObjectUtil.isNull(task)) {
throw new ServiceException("任务不存在!");
}
FlowNode flowNode = flowNodeMapper.selectOne(QueryBuilder.lambda(FlowNode.class)
.eq(FlowNode::getNodeCode, task.getNodeCode())
.eq(FlowNode::getDefinitionId, task.getDefinitionId())
.build());
if (ObjectUtil.isNull(flowNode)) {
throw new ServiceException("流程节点不存在");
}
if ((context.getOperation() == TaskOperationEnum.ADD_SIGNATURE || context.getOperation() == TaskOperationEnum.REDUCTION_SIGNATURE)
&& CooperateType.isOrSign(flowNode.getNodeRatio())) {
throw new ServiceException(task.getNodeName() + "不是会签或票签节点!");
}
context.setTask(task);
context.setFlowNode(flowNode);
}
}
@@ -0,0 +1,24 @@
package org.dromara.workflow.liteflow.operation;
import cn.hutool.core.collection.CollUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeBooleanComponent;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.context.TaskOperationContext;
/**
* 判断任务操作后是否进入消息通知分支
*
* @author may
*/
@ConditionalOnEnable
@LiteflowComponent("taskOpNeedNotify")
public class TaskOpNeedNotifyComponent extends NodeBooleanComponent {
@Override
public boolean processBoolean() {
TaskOperationContext context = getContextBean(TaskOperationContext.class);
return context.isResult() && CollUtil.isNotEmpty(context.getTaskOperationBo().getMessageType());
}
}
@@ -0,0 +1,57 @@
package org.dromara.workflow.liteflow.operation;
import cn.hutool.core.collection.CollUtil;
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.utils.StringUtils;
import org.dromara.system.api.UserService;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.bo.TaskOperationBo;
import org.dromara.workflow.domain.context.TaskOperationContext;
import org.dromara.workflow.service.IFlwCommonService;
import java.util.ArrayList;
import java.util.List;
import static org.dromara.workflow.common.constant.FlowConstant.PATH_TASK_WAITING;
/**
* 任务操作成功后发送待办消息
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("taskOpNotify")
public class TaskOpNotifyComponent extends NodeComponent {
private final IFlwCommonService flwCommonService;
private final UserService userService;
@Override
public void process() {
TaskOperationContext context = getContextBean(TaskOperationContext.class);
TaskOperationBo bo = context.getTaskOperationBo();
List<Long> userIdList = new ArrayList<>();
if (StringUtils.isNotBlank(bo.getUserId())) {
userIdList.add(Convert.toLong(bo.getUserId()));
}
if (CollUtil.isNotEmpty(bo.getUserIds())) {
userIdList.addAll(bo.getUserIds().stream().map(Convert::toLong).toList());
}
if (CollUtil.isEmpty(userIdList)) {
return;
}
// 转办委托加减签等运行时操作消息接收人统一从我的待办进入处理
flwCommonService.sendMessage(
bo.getMessageType(),
StringUtils.isNotBlank(bo.getMessage()) ? bo.getMessage() : "单据「" + context.getOperation().getDesc() + "」通知",
"单据「" + context.getOperation().getDesc() + "」提醒",
userService.selectListByIds(userIdList),
PATH_TASK_WAITING
);
}
}
@@ -0,0 +1,57 @@
package org.dromara.workflow.liteflow.operation;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
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.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;
/**
* 准备任务操作参数
*
* @author may
*/
@ConditionalOnEnable
@Slf4j
@LiteflowComponent("taskOpPrepare")
public class TaskOpPrepareComponent extends NodeComponent {
@Override
public void process() {
TaskOperationContext context = getContextBean(TaskOperationContext.class);
TaskOperationEnum op = TaskOperationEnum.getByCode(context.getTaskOperation());
if (op == null) {
log.error("Invalid operation type:{} ", context.getTaskOperation());
throw new ServiceException("Invalid operation type " + context.getTaskOperation());
}
context.setOperation(op);
TaskOperationBo bo = context.getTaskOperationBo();
switch (op) {
case DELEGATE_TASK, TRANSFER_TASK -> ValidatorUtils.validate(bo, AddGroup.class);
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);
}
}
@@ -0,0 +1,67 @@
package org.dromara.workflow.liteflow.start;
import cn.hutool.core.collection.CollUtil;
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.common.core.exception.ServiceException;
import org.dromara.warm.flow.core.dto.FlowParams;
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.service.TaskService;
import org.dromara.warm.flow.orm.entity.FlowTask;
import org.dromara.workflow.api.domain.StartProcessReturnDTO;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.FlowInstanceBizExt;
import org.dromara.workflow.domain.context.StartProcessContext;
import org.dromara.workflow.mapper.FlwInstanceBizExtMapper;
import java.util.List;
/**
* 启动流程实例并构建返回结果
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@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);
context.setInstance(instance);
saveBizExt(context, instance);
List<Task> taskList = taskService.list(new FlowTask().setInstanceId(instance.getId()));
if (CollUtil.isEmpty(taskList)) {
throw new ServiceException("流程启动失败,未生成任务");
}
if (taskList.size() > 1) {
throw new ServiceException("请检查流程第一个环节是否为申请人!");
}
context.setTaskList(taskList);
context.setStartProcessReturn(new StartProcessReturnDTO(instance.getId(), taskList.getFirst().getId()));
}
private void saveBizExt(StartProcessContext context, Instance instance) {
FlowInstanceBizExt bizExt = context.getBizExt();
bizExt.setInstanceId(instance.getId());
bizExt.setBusinessId(instance.getBusinessId());
flwInstanceBizExtMapper.saveOrUpdateByInstanceId(bizExt);
}
}
@@ -0,0 +1,24 @@
package org.dromara.workflow.liteflow.start;
import cn.hutool.core.util.ObjectUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeBooleanComponent;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.context.StartProcessContext;
/**
* 判断启动流程时是否走已有实例续提交分支
*
* @author may
*/
@ConditionalOnEnable
@LiteflowComponent("startExists")
public class StartExistsComponent extends NodeBooleanComponent {
@Override
public boolean processBoolean() {
StartProcessContext context = getContextBean(StartProcessContext.class);
return ObjectUtil.isNotNull(context.getExistingInstance());
}
}
@@ -0,0 +1,55 @@
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;
import org.dromara.workflow.domain.FlowInstanceBizExt;
import org.dromara.workflow.domain.context.StartProcessContext;
import java.util.Map;
import static org.dromara.workflow.common.constant.FlowConstant.*;
/**
* 加载流程定义并补齐启动变量
*
* @author may
*/
@ConditionalOnEnable
@LiteflowComponent("startPrepareInstance")
public class StartPrepareInstanceComponent extends NodeComponent {
@Override
public void process() {
StartProcessContext context = getContextBean(StartProcessContext.class);
Definition definition = FlowEngine.defService().getPublishByFlowCode(context.getStartProcessBo().getFlowCode());
if (ObjectUtil.isNull(definition)) {
throw new ServiceException("流程【" + context.getStartProcessBo().getFlowCode() + "】未发布,请先在流程设计器中发布流程定义");
}
context.setDefinition(definition);
Map<String, Object> 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()));
}
private String generateBusinessCode(FlowInstanceBizExt bizExt) {
if (ObjectUtil.isNull(bizExt.getBusinessCode()) || bizExt.getBusinessCode().isBlank()) {
// TODO: 按照自己业务规则生成编号
String businessCode = Convert.toStr(System.currentTimeMillis());
bizExt.setBusinessCode(businessCode);
return businessCode;
}
return bizExt.getBusinessCode();
}
}
@@ -0,0 +1,53 @@
package org.dromara.workflow.liteflow.start;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.exception.ServiceException;
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.orm.entity.FlowInstance;
import org.dromara.warm.flow.orm.mapper.FlowInstanceMapper;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.context.StartProcessContext;
import java.util.Map;
import static org.dromara.workflow.common.constant.FlowConstant.*;
/**
* 准备流程启动请求
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("startPrepareRequest")
public class StartPrepareRequestComponent extends NodeComponent {
private final FlowInstanceMapper flowInstanceMapper;
@Override
public void process() {
StartProcessContext context = getContextBean(StartProcessContext.class);
String businessId = context.getStartProcessBo().getBusinessId();
if (StringUtils.isBlank(businessId)) {
throw new ServiceException("启动工作流时必须包含业务ID");
}
context.setBusinessId(businessId);
context.setVariables(context.getStartProcessBo().getVariables());
context.setBizExt(context.getStartProcessBo().getBizExt());
Map<String, Object> variables = context.getVariables();
variables.put(INITIATOR, LoginHelper.getUserIdStr());
variables.put(INITIATOR_DEPT_ID, LoginHelper.getDeptId());
variables.put(BUSINESS_ID, businessId);
FlowInstance flowInstance = flowInstanceMapper.selectOne(QueryBuilder.lambda(FlowInstance.class)
.eq(FlowInstance::getBusinessId, businessId)
.build());
context.setExistingInstance(flowInstance);
}
}
@@ -0,0 +1,56 @@
package org.dromara.workflow.liteflow.start;
import cn.hutool.core.collection.CollUtil;
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.common.core.exception.ServiceException;
import org.dromara.warm.flow.core.entity.Task;
import org.dromara.warm.flow.core.service.InsService;
import org.dromara.warm.flow.core.service.TaskService;
import org.dromara.warm.flow.orm.entity.FlowTask;
import org.dromara.workflow.api.domain.StartProcessReturnDTO;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.FlowInstanceBizExt;
import org.dromara.workflow.domain.context.StartProcessContext;
import org.dromara.workflow.mapper.FlwInstanceBizExtMapper;
import java.util.List;
/**
* 已存在流程实例时执行续提交
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("startResume")
public class StartResumeComponent extends NodeComponent {
private final TaskService taskService;
private final InsService insService;
private final FlwInstanceBizExtMapper flwInstanceBizExtMapper;
@Override
public void process() {
StartProcessContext context = getContextBean(StartProcessContext.class);
BusinessStatusEnum.checkStartStatus(context.getExistingInstance().getFlowStatus());
List<Task> taskList = taskService.list(new FlowTask().setInstanceId(context.getExistingInstance().getId()));
if (CollUtil.isEmpty(taskList)) {
throw new ServiceException("流程实例缺少任务,请检查流程定义配置");
}
taskService.mergeVariable(context.getExistingInstance(), context.getVariables());
insService.updateById(context.getExistingInstance());
saveBizExt(context);
context.setStartProcessReturn(new StartProcessReturnDTO(taskList.getFirst().getInstanceId(), taskList.getFirst().getId()));
}
private void saveBizExt(StartProcessContext context) {
FlowInstanceBizExt bizExt = context.getBizExt();
bizExt.setInstanceId(context.getExistingInstance().getId());
bizExt.setBusinessId(context.getExistingInstance().getBusinessId());
flwInstanceBizExtMapper.saveOrUpdateByInstanceId(bizExt);
}
}
@@ -14,6 +14,7 @@ import org.dromara.common.core.enums.BusinessStatusEnum;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StreamUtils;
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.LambdaJoinQueryBuilder;
import org.dromara.common.mybatis.core.query.QueryBuilder;
@@ -41,9 +42,9 @@ import org.dromara.workflow.domain.bo.FlowCancelBo;
import org.dromara.workflow.domain.bo.FlowInstanceBo;
import org.dromara.workflow.domain.bo.FlowInvalidBo;
import org.dromara.workflow.domain.bo.FlowVariableBo;
import org.dromara.workflow.domain.context.InstanceDeleteContext;
import org.dromara.workflow.domain.vo.FlowHisTaskVo;
import org.dromara.workflow.domain.vo.FlowInstanceVo;
import org.dromara.workflow.handler.FlowProcessEventHandler;
import org.dromara.workflow.mapper.FlwCategoryMapper;
import org.dromara.workflow.mapper.FlwInstanceMapper;
import org.dromara.workflow.service.IFlwInstanceService;
@@ -52,7 +53,6 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.function.Function;
/**
* 流程实例 服务层实现
@@ -65,12 +65,13 @@ import java.util.function.Function;
@Service
public class FlwInstanceServiceImpl implements IFlwInstanceService {
private static final String DELETE_INSTANCE_CHAIN = "deleteInstanceChain";
private final InsService insService;
private final DefService defService;
private final TaskService taskService;
private final FlowHisTaskMapper flowHisTaskMapper;
private final FlowInstanceMapper flowInstanceMapper;
private final FlowProcessEventHandler flowProcessEventHandler;
private final IFlwTaskService flwTaskService;
private final FlwInstanceMapper flwInstanceMapper;
private final FlwCategoryMapper flwCategoryMapper;
@@ -211,17 +212,9 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteByBusinessIds(List<String> businessIds) {
List<FlowInstance> flowInstances = flowInstanceMapper.selectList(
QueryBuilder.lambda(FlowInstance.class)
.in(FlowInstance::getBusinessId, businessIds)
.build());
if (CollUtil.isEmpty(flowInstances)) {
log.warn("未找到对应的流程实例信息,无法执行删除操作。");
return false;
}
// 发送事件
processDeleteHandler(flowInstances);
return insService.remove(StreamUtils.toList(flowInstances, FlowInstance::getId));
InstanceDeleteContext context = InstanceDeleteContext.byBusinessIds(businessIds);
LiteFlowUtils.execute(DELETE_INSTANCE_CHAIN, context);
return context.isResult();
}
/**
@@ -233,16 +226,9 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteByInstanceIds(Collection<Long> instanceIds) {
// 获取实例信息
List<FlowInstance> flowInstances = flowInstanceMapper.selectByIds(instanceIds);
if (CollUtil.isEmpty(flowInstances)) {
log.warn("未找到对应的流程实例信息,无法执行删除操作。");
return false;
}
// 发送事件
processDeleteHandler(flowInstances);
// 删除实例
return insService.remove((List<Long>) instanceIds);
InstanceDeleteContext context = InstanceDeleteContext.byInstanceIds(instanceIds);
LiteFlowUtils.execute(DELETE_INSTANCE_CHAIN, context);
return context.isResult();
}
/**
@@ -254,56 +240,9 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteHisByInstanceIds(Collection<Long> instanceIds) {
// 获取实例信息
List<FlowInstance> flowInstances = flowInstanceMapper.selectByIds(instanceIds);
if (CollUtil.isEmpty(flowInstances)) {
log.warn("未找到对应的流程实例信息,无法执行删除操作。");
return false;
}
// 发送事件
processDeleteHandler(flowInstances);
List<FlowTask> flowTaskList = flwTaskService.selectByInstIds(instanceIds);
if (CollUtil.isNotEmpty(flowTaskList)) {
FlowEngine.userService().deleteByTaskIds(StreamUtils.toList(flowTaskList, FlowTask::getId));
}
FlowEngine.taskService().deleteByInsIds((List<Long>) instanceIds);
FlowEngine.hisTaskService().deleteByInsIds((List<Long>) instanceIds);
FlowEngine.insService().removeByIds(instanceIds);
return true;
}
/**
* 校验当前用户是否有权限删除流程实例并发布删除事件
*
* @param flowInstances 待删除流程实例列表
*/
private void processDeleteHandler(List<FlowInstance> flowInstances) {
String userId = LoginHelper.getUserIdStr();
for (FlowInstance flowInstance : flowInstances) {
//如果创建人与当前登陆人一致或者当前登陆人为管理员才能删除
if (LoginHelper.isSuperAdmin() || flowInstance.getCreateBy().equals(userId)) {
continue;
}
throw new ServiceException("权限不足,无法删除流程实例信息!");
}
// 获取定义信息
Map<Long, Definition> definitionMap = StreamUtils.toMap(
defService.getByIds(StreamUtils.toList(flowInstances, Instance::getDefinitionId)),
Definition::getId,
Function.identity()
);
// 逐一触发删除事件
flowInstances.forEach(instance -> {
Definition definition = definitionMap.get(instance.getDefinitionId());
if (ObjectUtil.isNull(definition)) {
log.warn("实例 ID: {} 对应的流程定义信息未找到,跳过删除事件触发。", instance.getId());
return;
}
flowProcessEventHandler.processDeleteHandler(definition.getFlowCode(), instance.getBusinessId());
});
InstanceDeleteContext context = InstanceDeleteContext.byHistoryInstanceIds(instanceIds);
LiteFlowUtils.execute(DELETE_INSTANCE_CHAIN, context);
return context.isResult();
}
/**
@@ -327,10 +266,9 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
if (!LoginHelper.isSuperAdmin() && !instance.getCreateBy().equals(userIdStr)) {
throw new ServiceException("权限不足,无法撤销流程!");
}
String message = bo.message();
BusinessStatusEnum.checkCancelStatus(instance.getFlowStatus());
FlowParams flowParams = FlowParams.build()
.message(message)
.message(bo.message())
.flowStatus(BusinessStatusEnum.CANCEL.getStatus())
.hisStatus(BusinessStatusEnum.CANCEL.getStatus())
.handler(userIdStr)
@@ -3,7 +3,6 @@ package org.dromara.workflow.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.lock.annotation.Lock4j;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -14,10 +13,7 @@ import org.dromara.common.core.enums.BusinessStatusEnum;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
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.json.utils.JsonUtils;
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;
@@ -25,34 +21,38 @@ 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.constant.ExceptionCons;
import org.dromara.warm.flow.core.dto.FlowCombine;
import org.dromara.warm.flow.core.dto.FlowParams;
import org.dromara.warm.flow.core.entity.*;
import org.dromara.warm.flow.core.enums.CooperateType;
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.utils.ExpressionUtil;
import org.dromara.warm.flow.core.utils.MapUtil;
import org.dromara.warm.flow.orm.entity.*;
import org.dromara.warm.flow.orm.entity.FlowHisTask;
import org.dromara.warm.flow.orm.entity.FlowNode;
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.FlowInstanceMapper;
import org.dromara.warm.flow.orm.mapper.FlowNodeMapper;
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.TaskOperationEnum;
import org.dromara.workflow.common.enums.TaskStatusEnum;
import org.dromara.workflow.domain.FlowInstanceBizExt;
import org.dromara.workflow.domain.bo.*;
import org.dromara.workflow.domain.context.CompleteTaskContext;
import org.dromara.workflow.domain.context.StartProcessContext;
import org.dromara.workflow.domain.context.TaskOperationContext;
import org.dromara.workflow.domain.vo.FlowCopyVo;
import org.dromara.workflow.domain.vo.FlowHisTaskVo;
import org.dromara.workflow.domain.vo.FlowTaskVo;
import org.dromara.workflow.domain.vo.NodeExtVo;
import org.dromara.workflow.mapper.FlwCategoryMapper;
import org.dromara.workflow.mapper.FlwHisTaskMapper;
import org.dromara.workflow.mapper.FlwInstanceBizExtMapper;
import org.dromara.workflow.mapper.FlwTaskMapper;
import org.dromara.workflow.service.IFlwCommonService;
import org.dromara.workflow.service.IFlwNodeExtService;
@@ -76,12 +76,15 @@ import static org.dromara.workflow.common.constant.FlowConstant.*;
@Service
public class FlwTaskServiceImpl implements IFlwTaskService {
private static final String START_PROCESS_CHAIN = "startProcessChain";
private static final String COMPLETE_TASK_CHAIN = "completeTaskChain";
private static final String TASK_OPERATION_CHAIN = "taskOperationChain";
private final TaskService taskService;
private final InsService insService;
private final DefService defService;
private final HisTaskService hisTaskService;
private final NodeService nodeService;
private final FlowInstanceMapper flowInstanceMapper;
private final FlowTaskMapper flowTaskMapper;
private final FlowHisTaskMapper flowHisTaskMapper;
private final UserService userService;
@@ -92,7 +95,6 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
private final IFlwTaskAssigneeService flwTaskAssigneeService;
private final IFlwCommonService flwCommonService;
private final IFlwNodeExtService flwNodeExtService;
private final FlwInstanceBizExtMapper flwInstanceBizExtMapper;
/**
* 启动任务
@@ -104,95 +106,9 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
@Transactional(rollbackFor = Exception.class)
@Lock4j(keys = {"#startProcessBo.flowCode + #startProcessBo.businessId"})
public StartProcessReturnDTO startWorkFlow(StartProcessBo startProcessBo) {
String businessId = startProcessBo.getBusinessId();
if (StringUtils.isBlank(businessId)) {
throw new ServiceException("启动工作流时必须包含业务ID");
}
// 启动流程实例提交申请
Map<String, Object> variables = startProcessBo.getVariables();
// 流程发起人
variables.put(INITIATOR, LoginHelper.getUserIdStr());
// 发起人部门id
variables.put(INITIATOR_DEPT_ID, LoginHelper.getDeptId());
// 业务id
variables.put(BUSINESS_ID, businessId);
FlowInstanceBizExt bizExt = startProcessBo.getBizExt();
// 获取已有流程实例
FlowInstance flowInstance = flowInstanceMapper.selectOne(QueryBuilder.lambda(FlowInstance.class)
.eq(FlowInstance::getBusinessId, businessId)
.build());
if (ObjectUtil.isNotNull(flowInstance)) {
// 已存在流程
BusinessStatusEnum.checkStartStatus(flowInstance.getFlowStatus());
List<Task> taskList = taskService.list(new FlowTask().setInstanceId(flowInstance.getId()));
if (CollUtil.isEmpty(taskList)) {
throw new ServiceException("流程实例缺少任务,请检查流程定义配置");
}
taskService.mergeVariable(flowInstance, variables);
insService.updateById(flowInstance);
StartProcessReturnDTO dto = new StartProcessReturnDTO(taskList.getFirst().getInstanceId(), taskList.getFirst().getId());
// 保存流程实例业务信息
this.buildFlowInstanceBizExt(flowInstance, bizExt);
return dto;
}
// 将流程定义内的扩展参数设置到变量中
Definition definition = FlowEngine.defService().getPublishByFlowCode(startProcessBo.getFlowCode());
if (ObjectUtil.isNull(definition)) {
throw new ServiceException("流程【" + startProcessBo.getFlowCode() + "】未发布,请先在流程设计器中发布流程定义");
}
Dict dict = JsonUtils.parseMap(definition.getExt());
boolean autoPass = !ObjectUtil.isNull(dict) && dict.getBool(AUTO_PASS);
variables.put(AUTO_PASS, autoPass);
variables.put(BUSINESS_CODE, this.generateBusinessCode(bizExt));
FlowParams flowParams = FlowParams.build()
.handler(startProcessBo.getHandler())
.flowCode(startProcessBo.getFlowCode())
.variable(startProcessBo.getVariables())
.flowStatus(BusinessStatusEnum.DRAFT.getStatus());
Instance instance = insService.start(businessId, flowParams);
// 保存流程实例业务信息
this.buildFlowInstanceBizExt(instance, bizExt);
// 申请人执行流程
List<Task> taskList = taskService.list(new FlowTask().setInstanceId(instance.getId()));
if (CollUtil.isEmpty(taskList)) {
throw new ServiceException("流程启动失败,未生成任务");
}
if (taskList.size() > 1) {
throw new ServiceException("请检查流程第一个环节是否为申请人!");
}
return new StartProcessReturnDTO(instance.getId(), taskList.getFirst().getId());
}
/**
* 生成业务编号如果已有则直接返回已有值
*
* @param bizExt 流程业务扩展信息
* @return 可用于流程实例展示的业务编号
*/
private String generateBusinessCode(FlowInstanceBizExt bizExt) {
if (StringUtils.isBlank(bizExt.getBusinessCode())) {
// TODO: 按照自己业务规则生成编号
String businessCode = Convert.toStr(System.currentTimeMillis());
bizExt.setBusinessCode(businessCode);
return businessCode;
}
return bizExt.getBusinessCode();
}
/**
* 构建流程实例业务信息
*
* @param instance 流程实例
* @param bizExt 流程业务扩展信息
*/
private void buildFlowInstanceBizExt(Instance instance, FlowInstanceBizExt bizExt) {
bizExt.setInstanceId(instance.getId());
bizExt.setBusinessId(instance.getBusinessId());
flwInstanceBizExtMapper.saveOrUpdateByInstanceId(bizExt);
StartProcessContext context = new StartProcessContext(startProcessBo);
LiteFlowUtils.execute(START_PROCESS_CHAIN, context);
return context.getStartProcessReturn();
}
/**
@@ -205,127 +121,11 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
@Transactional(rollbackFor = Exception.class)
@Lock4j(keys = {"#completeTaskBo.taskId"})
public boolean completeTask(CompleteTaskBo completeTaskBo) {
// 获取任务ID并查询对应的流程任务和实例信息
Long taskId = completeTaskBo.getTaskId();
List<String> messageType = completeTaskBo.getMessageType();
String notice = completeTaskBo.getNotice();
// 获取抄送人
List<FlowCopyBo> flowCopyList = completeTaskBo.getFlowCopyList();
// 设置抄送人
Map<String, Object> variables = completeTaskBo.getVariables();
variables.put(FLOW_COPY_LIST, flowCopyList);
// 消息类型
variables.put(MESSAGE_TYPE, messageType);
// 消息通知
variables.put(MESSAGE_NOTICE, notice);
FlowTask flowTask = flowTaskMapper.selectById(taskId);
if (ObjectUtil.isNull(flowTask)) {
throw new ServiceException("流程任务不存在或任务已审批!");
}
Instance ins = insService.getById(flowTask.getInstanceId());
if (ObjectUtil.isNull(ins)) {
throw new ServiceException("流程实例不存在");
}
// 检查流程状态是否为草稿已撤销或已退回状态若是则执行流程提交监听
if (BusinessStatusEnum.isDraftOrCancelOrBack(ins.getFlowStatus())) {
variables.put(SUBMIT, true);
}
Map<String, Object> insVariableMap = ins.getVariableMap();
// 设置弹窗处理人
Map<String, Object> assigneeMap = setPopAssigneeMap(completeTaskBo.getAssigneeMap(), insVariableMap);
if (CollUtil.isNotEmpty(assigneeMap)) {
variables.putAll(assigneeMap);
}
// 构建流程参数包括变量跳转类型消息处理人权限等信息
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());
Boolean autoPass = Convert.toBool(insVariableMap.getOrDefault(AUTO_PASS, false));
skipTask(taskId, flowParams, flowTask.getInstanceId(), autoPass);
// 办理任务的业务编排交给 LiteFlow 链路当前方法只保留事务锁和异常透传边界
LiteFlowUtils.execute(COMPLETE_TASK_CHAIN, new CompleteTaskContext(completeTaskBo));
return true;
}
/**
* 流程办理
*
* @param taskId 任务ID
* @param flowParams 参数
* @param instanceId 实例ID
* @param autoPass 自动审批
*/
private void skipTask(Long taskId, FlowParams flowParams, Long instanceId, Boolean autoPass) {
// 执行任务跳转并根据返回的处理人设置下一步处理人
taskService.skip(taskId, flowParams);
List<FlowTask> flowTaskList = selectByInstId(instanceId);
if (CollUtil.isEmpty(flowTaskList)) {
return;
}
List<User> userList = FlowEngine.userService()
.getByAssociateds(StreamUtils.toList(flowTaskList, FlowTask::getId));
if (CollUtil.isEmpty(userList)) {
return;
}
for (FlowTask task : flowTaskList) {
if (!task.getId().equals(taskId) && autoPass) {
List<User> 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));
skipTask(task.getId(), flowParams, instanceId, true);
}
}
}
/**
* 设置弹窗处理人
*
* @param assigneeMap 处理人
* @param variablesMap 变量
* @return 追加流程状态前缀后的处理人变量映射
*/
private Map<String, Object> setPopAssigneeMap(Map<String, Object> assigneeMap, Map<String, Object> variablesMap) {
Map<String, Object> map = new HashMap<>();
if (CollUtil.isEmpty(assigneeMap)) {
return map;
}
for (Map.Entry<String, Object> entry : assigneeMap.entrySet()) {
if (variablesMap.containsKey(entry.getKey())) {
String userIds = variablesMap.get(entry.getKey()).toString();
if (StringUtils.isNotBlank(userIds)) {
Set<String> hashSet = new HashSet<>();
//弹窗传入的选人
List<String> popUserIds = StringUtils.str2List(entry.getValue().toString(), StringUtils.SEPARATOR, true, true);
//已有的选人
List<String> variableUserIds = StringUtils.str2List(userIds, StringUtils.SEPARATOR, true, true);
hashSet.addAll(popUserIds);
hashSet.addAll(variableUserIds);
map.put(TaskStatusEnum.PASS.getStatus() + StringUtils.COLON + entry.getKey(), StringUtils.joinComma(hashSet));
map.put(TaskStatusEnum.BACK.getStatus() + 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());
}
}
return map;
}
/**
* 添加抄送人
*
@@ -504,9 +304,7 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
String applyNodeCode = flwCommonService.applyNodeCode(definitionId);
Map<String, Object> variable = new HashMap<>();
// 消息类型
variable.put(MESSAGE_TYPE, messageType);
// 消息通知
variable.put(MESSAGE_NOTICE, notice);
FlowParams flowParams = FlowParams.build()
@@ -549,16 +347,30 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
return nodeService.getByNodeCodes(Collections.singletonList(node.getAnyNodeSkip()), task.getDefinitionId());
}
//获取可驳回的前置节点
List<Node> nodes = nodeService.previousNodeList(task.getDefinitionId(), nowNodeCode);
Long definitionId = task.getDefinitionId();
FlowCombine flowCombine = defService.getFlowCombineNoDef(definitionId);
Map<String, Node> nodeMap = getPreviousNodeMap(nowNodeCode, flowCombine);
List<HisTask> hisTaskList = hisTaskService.getByInsId(task.getInstanceId());
Map<String, Node> nodeMap = StreamUtils.toIdentityMap(nodes, Node::getNodeCode);
Set<String> reachableNodeCodes = new HashSet<>();
if (CollUtil.isNotEmpty(hisTaskList)) {
Instance instance = insService.getById(task.getInstanceId());
if (ObjectUtil.isNull(instance)) {
throw new ServiceException("流程实例不存在");
}
collectReachableNodeCodes(reachableNodeCodes, instance, flowCombine);
if (!reachableNodeCodes.contains(nowNodeCode)) {
reachableNodeCodes.clear();
}
}
Set<String> added = new HashSet<>();
List<Node> backNodeList = new ArrayList<>();
for (HisTask hisTask : hisTaskList) {
Node nodeValue = nodeMap.get(hisTask.getNodeCode());
if (nodeValue != null
&& NodeType.BETWEEN.getKey().equals(nodeValue.getNodeType())
&& (CollUtil.isEmpty(reachableNodeCodes) || reachableNodeCodes.contains(nodeValue.getNodeCode()))
&& added.add(nodeValue.getNodeCode())) {
backNodeList.add(nodeValue);
}
@@ -567,7 +379,78 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
Collections.reverse(backNodeList);
return backNodeList;
}
return nodes;
if (CollUtil.isNotEmpty(hisTaskList)) {
return Collections.emptyList();
}
return StreamUtils.filter(nodeMap.values(), e -> NodeType.BETWEEN.getKey().equals(e.getNodeType()));
}
private Map<String, Node> getPreviousNodeMap(String nodeCode, FlowCombine flowCombine) {
if (ObjectUtil.isNull(flowCombine) || CollUtil.isEmpty(flowCombine.getAllNodes()) || CollUtil.isEmpty(flowCombine.getAllSkips())) {
return Collections.emptyMap();
}
Map<String, Node> allNodeMap = StreamUtils.toIdentityMap(flowCombine.getAllNodes(), Node::getNodeCode);
Map<String, List<Skip>> previousSkipMap = new HashMap<>();
for (Skip skip : flowCombine.getAllSkips()) {
previousSkipMap.computeIfAbsent(skip.getNextNodeCode(), k -> new ArrayList<>()).add(skip);
}
Map<String, Node> previousNodeMap = new LinkedHashMap<>();
Set<String> visitedNodeCodes = new HashSet<>();
Deque<String> nodeQueue = new ArrayDeque<>();
visitedNodeCodes.add(nodeCode);
nodeQueue.add(nodeCode);
while (CollUtil.isNotEmpty(nodeQueue)) {
String currentNodeCode = nodeQueue.poll();
List<Skip> previousSkips = previousSkipMap.get(currentNodeCode);
if (CollUtil.isEmpty(previousSkips)) {
continue;
}
for (Skip previousSkip : previousSkips) {
String previousNodeCode = previousSkip.getNowNodeCode();
if (!visitedNodeCodes.add(previousNodeCode)) {
continue;
}
Node previousNode = allNodeMap.get(previousNodeCode);
if (ObjectUtil.isNull(previousNode)) {
continue;
}
previousNodeMap.put(previousNodeCode, previousNode);
nodeQueue.add(previousNodeCode);
}
}
return previousNodeMap;
}
private void collectReachableNodeCodes(Set<String> nodeCodes, Instance instance, FlowCombine flowCombine) {
if (ObjectUtil.isNull(flowCombine) || CollUtil.isEmpty(flowCombine.getAllNodes())) {
return;
}
Deque<Node> nodeQueue = new ArrayDeque<>();
flowCombine.getAllNodes().stream()
.filter(e -> NodeType.START.getKey().equals(e.getNodeType()))
.findFirst()
.ifPresent(nodeQueue::add);
while (CollUtil.isNotEmpty(nodeQueue)) {
Node currentNode = nodeQueue.poll();
if (ObjectUtil.isNull(currentNode) || !nodeCodes.add(currentNode.getNodeCode())) {
continue;
}
try {
List<Node> nextNodes = nodeService.getNextNodeList(currentNode, null, SkipType.PASS.getKey(),
instance.getVariableMap(), null, flowCombine);
if (CollUtil.isNotEmpty(nextNodes)) {
nodeQueue.addAll(nextNodes);
}
} catch (FlowException e) {
// 条件变量缺失时跳过当前分支其他引擎异常继续抛出
if (!StringUtils.containsAny(e.getMessage(), ExceptionCons.NULL_CONDITION_VALUE, ExceptionCons.NULL_SKIP_TYPE)) {
throw e;
}
}
}
}
/**
@@ -775,90 +658,9 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean taskOperation(TaskOperationBo bo, String taskOperation) {
TaskOperationEnum op = TaskOperationEnum.getByCode(taskOperation);
if (op == null) {
log.error("Invalid operation type:{} ", taskOperation);
throw new ServiceException("Invalid operation type " + taskOperation);
}
FlowParams flowParams = FlowParams.build().message(bo.getMessage());
if (LoginHelper.isSuperAdmin()) {
flowParams.ignore(true);
}
// 根据操作类型构建 FlowParams
switch (op) {
case DELEGATE_TASK, TRANSFER_TASK -> {
ValidatorUtils.validate(bo, AddGroup.class);
flowParams.addHandlers(Collections.singletonList(bo.getUserId()));
}
case ADD_SIGNATURE -> {
ValidatorUtils.validate(bo, EditGroup.class);
flowParams.addHandlers(bo.getUserIds());
}
case REDUCTION_SIGNATURE -> {
ValidatorUtils.validate(bo, EditGroup.class);
flowParams.reductionHandlers(bo.getUserIds());
}
}
Long taskId = bo.getTaskId();
Task task = taskService.getById(taskId);
if (ObjectUtil.isNull(task)) {
throw new ServiceException("任务不存在!");
}
FlowNode flowNode = getByNodeCode(task.getNodeCode(), task.getDefinitionId());
if (ObjectUtil.isNull(flowNode)) {
throw new ServiceException("流程节点不存在");
}
if (op == TaskOperationEnum.ADD_SIGNATURE || op == TaskOperationEnum.REDUCTION_SIGNATURE) {
if (CooperateType.isOrSign(flowNode.getNodeRatio())) {
throw new ServiceException(task.getNodeName() + "不是会签或票签节点!");
}
}
// 设置任务状态并执行对应的任务操作
boolean result = false;
switch (op) {
case DELEGATE_TASK -> {
flowParams.hisStatus(TaskStatusEnum.DEPUTE.getStatus());
result = taskService.depute(taskId, flowParams);
}
case TRANSFER_TASK -> {
flowParams.hisStatus(TaskStatusEnum.TRANSFER.getStatus());
result = taskService.transfer(taskId, flowParams);
}
case ADD_SIGNATURE -> {
flowParams.hisStatus(TaskStatusEnum.SIGN.getStatus());
result = taskService.addSignature(taskId, flowParams);
}
case REDUCTION_SIGNATURE -> {
flowParams.hisStatus(TaskStatusEnum.SIGN_OFF.getStatus());
result = taskService.reductionSignature(taskId, flowParams);
}
}
// 操作执行成功后再发送消息
if (result && CollUtil.isNotEmpty(bo.getMessageType())) {
List<Long> userIdList = new ArrayList<>();
if (StringUtils.isNotBlank(bo.getUserId())) {
userIdList.add(Convert.toLong(bo.getUserId()));
}
if (CollUtil.isNotEmpty(bo.getUserIds())) {
userIdList.addAll(StreamUtils.toList(bo.getUserIds(), Convert::toLong));
}
if (CollUtil.isNotEmpty(userIdList)) {
// 转办委托加减签等运行时操作消息接收人统一从我的待办进入处理
flwCommonService.sendMessage(
bo.getMessageType(),
StringUtils.isNotBlank(bo.getMessage()) ? bo.getMessage() : "单据「" + op.getDesc() + "」通知",
"单据「" + op.getDesc() + "」提醒",
userService.selectListByIds(userIdList),
PATH_TASK_WAITING
);
}
}
return result;
TaskOperationContext context = new TaskOperationContext(bo, taskOperation);
LiteFlowUtils.execute(TASK_OPERATION_CHAIN, context);
return context.isResult();
}
/**

Some files were not shown because too many files have changed in this diff Show More