mirror of
https://github.com/dromara/RuoYi-Vue-Plus.git
synced 2026-08-27 13:57:13 +00:00
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7180b52977 | |||
| 070d0bdd45 | |||
| a2e3eda278 | |||
| ac61c0a558 | |||
| a562c78d27 | |||
| 071db648cd | |||
| 47a71acf49 | |||
| 46cef1c6c7 | |||
| d6760f93d5 | |||
| da5f30cae2 | |||
| 28c39d5b90 | |||
| d3146a5755 | |||
| e8f2e68f0e | |||
| c2667cfbe1 | |||
| 44a9bd1006 | |||
| 543037885d | |||
| d7eca3db85 | |||
| 348141427d | |||
| 4baa9edfa1 | |||
| a5b65da892 | |||
| 126380e9f5 | |||
| 23387d71d8 | |||
| 59f1e75bdc | |||
| c4b1e58c3e | |||
| d9894b534d | |||
| c8f755cbde | |||
| 53f857714d | |||
| 6876bd355f | |||
| 7ca18b7798 | |||
| dbbc790081 | |||
| 4e6dbee008 | |||
| e571f339b4 | |||
| 74ed2734f0 | |||
| 1fbe1694e4 | |||
| c56dc6b8e9 | |||
| 1be4f7b07e | |||
| 5392c3228e | |||
| 837604f889 | |||
| eab4228548 | |||
| e8b033c4ef | |||
| 3cf2eceb13 | |||
| 1d263f7656 | |||
| c1e344be0e | |||
| 612a965c26 | |||
| d7c525d7f8 | |||
| 93a07e57b8 | |||
| 642c713f78 | |||
| 123ba9bfba | |||
| 66b4840356 | |||
| 14e2cf16cc | |||
| f3256442c8 |
@@ -61,4 +61,4 @@ description: 标准后端 CRUD 专家。用于当前项目中的新增单表 CRU
|
||||
- BO / VO / Entity 是否职责分离
|
||||
- 导出、分页、删除前校验是否齐全
|
||||
- 是否只是 generator 裸产物,如果是要继续补齐项目约定
|
||||
- 前端 `api/types/index.vue` 如需同步,接口路径、返回结构、日期范围参数要与后端一致
|
||||
- 前端 `api/types` 和 Vue `index.vue` 或 React `index.tsx` 如需同步,接口路径、返回结构、日期范围参数要与后端一致
|
||||
|
||||
@@ -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 模板和真实代码冲突,优先相信当前模块真实代码和实际调用点。
|
||||
|
||||
@@ -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 页面。
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
# 是否锁定当前智能体
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
-124
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
+2
-2
@@ -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)) {
|
||||
|
||||
+3
-3
@@ -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 提供器。
|
||||
|
||||
+2
-2
@@ -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 提供器。
|
||||
|
||||
+5
-21
@@ -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>
|
||||
+19
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+19
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+22
@@ -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 上下文不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+40
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+19
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
+67
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package org.dromara.common.liteflow.core;
|
||||
|
||||
/**
|
||||
* LiteFlow 失败提示提供者。
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
public interface FailMessageProvider {
|
||||
|
||||
/**
|
||||
* 获取公共失败节点抛出的业务提示。
|
||||
*
|
||||
* @return 失败提示
|
||||
*/
|
||||
String getFailMessage();
|
||||
|
||||
}
|
||||
+42
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
org.dromara.common.liteflow.config.LiteFlowAutoConfiguration
|
||||
+7
-18
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+5
@@ -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);
|
||||
|
||||
+3
-1
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+106
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传本地路径文件到默认存储桶。
|
||||
*
|
||||
|
||||
+4
-14
@@ -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访问策略配置
|
||||
*/
|
||||
|
||||
+5
@@ -31,4 +31,9 @@ public interface MessageConstants {
|
||||
* 心跳响应标识
|
||||
*/
|
||||
String PONG = "pong";
|
||||
|
||||
/**
|
||||
* 同一 token 的新连接替换旧连接时发送给旧连接的控制消息。
|
||||
*/
|
||||
String KICKED = "kicked";
|
||||
}
|
||||
|
||||
+16
@@ -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 连接
|
||||
*
|
||||
|
||||
+14
@@ -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消息推送
|
||||
|
||||
+32
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
+1
@@ -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>
|
||||
|
||||
<!-- 核心模块 -->
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
+23
-17
@@ -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);
|
||||
|
||||
+11
-11
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
/**
|
||||
|
||||
-8
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为日期范围查询字段。
|
||||
*
|
||||
|
||||
+1
-1
@@ -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 类型。
|
||||
*
|
||||
|
||||
+49
-11
@@ -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
|
||||
|
||||
+7
-2
@@ -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
|
||||
|
||||
+7
-2
@@ -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;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
|
||||
+3
-3
@@ -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;
|
||||
}
|
||||
|
||||
+8
-1
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
-9
@@ -108,13 +108,4 @@ public interface FlowConstant {
|
||||
*/
|
||||
String VAR_IGNORE_COOPERATE = "ignoreCooperate";
|
||||
|
||||
/**
|
||||
* 未删除(正常数据)
|
||||
*/
|
||||
Integer NOT_DELETED = 0;
|
||||
|
||||
/**
|
||||
* 已删除(逻辑删除)
|
||||
*/
|
||||
Integer DELETED = 1;
|
||||
}
|
||||
|
||||
-1
@@ -63,7 +63,6 @@ public class FlwSpelController extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* /**
|
||||
* 新增流程 SpEL 表达式定义。
|
||||
*
|
||||
* @param bo 表达式信息
|
||||
|
||||
+5
-4
@@ -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));
|
||||
}
|
||||
|
||||
-1
@@ -53,7 +53,6 @@ public class TestLeaveController extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* /**
|
||||
* 导出请假列表。
|
||||
*
|
||||
* @param bo 查询条件
|
||||
|
||||
+44
@@ -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;
|
||||
|
||||
}
|
||||
+59
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+70
@@ -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;
|
||||
|
||||
}
|
||||
+55
@@ -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;
|
||||
|
||||
}
|
||||
+33
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
+95
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
+54
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+23
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
+100
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+60
@@ -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());
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
+79
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+24
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
+52
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+49
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+53
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+24
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
+57
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
+57
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+67
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+24
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
+55
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+53
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+56
@@ -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
-76
@@ -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)
|
||||
|
||||
+111
-309
@@ -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
Reference in New Issue
Block a user