Compare commits

...

67 Commits

Author SHA1 Message Date
疯狂的狮子Li 13e8ac5f85 update 优化 对mybatis游标查询 进行加解密支持 2026-08-26 09:38:00 +08:00
疯狂的狮子Li b37db3fd01 update 优化 增加 groupby 函数(感谢 喃风 贡献) 2026-08-24 15:07:11 +08:00
疯狂的狮子Li 629e344af5 fix 修复 数据权限对union语句不兼容问题 2026-08-24 11:48:56 +08:00
疯狂的狮子Li 2933badb91 update 优化 sql输出性能 去掉锁 2026-08-21 12:57:49 +08:00
YueYe 387c4f0a20 !880 [fix]修复 启用本地缓存
* [fix]修复 启用本地缓存
2026-08-20 06:37:54 +00:00
疯狂的狮子Li a16f249024 fix 修复 mapper.lambda 链式调用无法走 @DS 直接切换数据源问题 2026-08-17 13:16:17 +08:00
疯狂的狮子Li b9538b0d1c fix 修复 liteflow 关闭之后 有一些配置类没有关闭导致报错问题 临时修复等官方处理 2026-08-17 09:38:09 +08:00
疯狂的狮子Li 1df89481db update liteflow 2.16.1.2 2026-08-13 09:23:18 +08:00
疯狂的狮子Li 04ef4b1147 update 优化 更新依赖名称 2026-08-10 15:24:29 +08:00
疯狂的狮子Li 430478c040 update README.md 2026-08-07 13:08:31 +08:00
疯狂的狮子Li 70deffdf05 fix 修复 树表模板 没有导入parseTime问题 2026-08-07 09:21:24 +08:00
疯狂的狮子Li fba7b40075 update 优化 使用我们自行fork的justauth版本 彻底去除fastjson 2026-08-04 14:30:02 +08:00
Kayleigh Wang 6d8c0d6d47 !877 fix 修复代码生成模板导出逻辑与多余空行问题
* fix 修复代码生成模板导出逻辑与多余空行问题
2026-08-03 01:09:44 +00:00
疯狂的狮子Li d31082f4a7 fix 修复 更换依赖将之前修复的代码覆盖了 2026-07-31 14:15:00 +08:00
疯狂的狮子Li c76212003c fix 修复 主线程先虚拟线程关闭导致无法获取request问题 2026-07-31 12:03:25 +08:00
疯狂的狮子Li 41ea764a8c update 优化 独立snail-ai esclient版本号 2026-07-30 13:09:00 +08:00
疯狂的狮子Li 7180b52977 🦁🦁🦁发布 6.0.0 正式版 大版本发布!!! 2026-07-30 11:02:02 +08:00
疯狂的狮子Li 070d0bdd45 update snail-ai 1.1.1 2026-07-30 09:25:07 +08:00
疯狂的狮子Li a2e3eda278 update 优化 开放ai系统智能体市场 2026-07-29 10:50:50 +08:00
疯狂的狮子Li ac61c0a558 update snail-ai 1.1.0 2026-07-27 11:46:02 +08:00
疯狂的狮子Li a562c78d27 update snail-job 2.0.2 2026-07-24 09:34:14 +08:00
疯狂的狮子Li 071db648cd update 优化 兼容企业微信 大小写返回不一致的奇葩问题 2026-07-22 19:10:43 +08:00
疯狂的狮子Li 47a71acf49 update 优化 使用三方修改的justauth版本 去掉fastjson 2026-07-22 11:39:57 +08:00
疯狂的狮子Li 46cef1c6c7 update 优化 增加工作流内部异常拦截 返回框架标准信息 2026-07-20 18:03:27 +08:00
疯狂的狮子Li d6760f93d5 update 优化 删除流程实例同时删除抄送内容 2026-07-20 11:49:24 +08:00
疯狂的狮子Li da5f30cae2 update 升级 全部依赖版本与适配性代码修改 2026-07-16 18:39:40 +08:00
疯狂的狮子Li 28c39d5b90 update 优化 自动装配警告问题 2026-07-15 11:30:01 +08:00
疯狂的狮子Li d3146a5755 update 优化 bo去掉BaseEntity之后 不生成基础字段问题 2026-07-15 09:44:03 +08:00
AprilWind e8f2e68f0e !871 update 优化数据库字段类型处理
* update 扩展数据库字段类型支持,增加浮点型和其他类型的规范化处理
2026-07-08 06:55:18 +00:00
疯狂的狮子Li c2667cfbe1 fix 修复 liteflow 公共节点包扫描问题 2026-07-08 12:08:36 +08:00
疯狂的狮子Li 44a9bd1006 update 优化 liteflow 整体上下文用法优化 2026-07-07 15:48:55 +08:00
AprilWind 543037885d !869 update 移除冗余注释和未使用的变量
* update 移除冗余注释和未使用的变量
2026-07-07 06:46:54 +00:00
疯狂的狮子Li d7eca3db85 update 优化 适配多前端 skill 内容 2026-07-06 16:15:41 +08:00
tangyh 348141427d !868 fix: 修复菜单构建方法返回空列表的情况,确保在无菜单时返回空集合
* fix: 修复菜单构建方法返回空列表的情况,确保在无菜单时返回空集合
2026-07-06 01:24:29 +00:00
疯狂的狮子Li 4baa9edfa1 update 优化 token过期提醒 2026-07-06 09:23:23 +08:00
疯狂的狮子Li a5b65da892 fix 修复 请求流程删除失败问题 2026-07-03 18:30:40 +08:00
疯狂的狮子Li 126380e9f5 fix 修复 用户上传头像后无法编辑问题 头像url改为独立字段避免影响 2026-07-03 17:00:38 +08:00
疯狂的狮子Li 23387d71d8 fix 修复 warmflow 时间用的还是Date类 导致无法注入数据问题 2026-07-03 16:24:25 +08:00
疯狂的狮子Li 59f1e75bdc fix 修复 删除sqlserver内多余的租户id 2026-07-03 11:02:49 +08:00
疯狂的狮子Li c4b1e58c3e update 优化 gen模块 默认激活 避免很多人不知道这个配置 2026-07-02 14:41:27 +08:00
疯狂的狮子Li d9894b534d update: 基于 LiteFlow 重构工作流业务编排 2026-07-02 14:39:50 +08:00
疯狂的狮子Li c8f755cbde add: 新增工作流 LiteFlow 规则链 2026-07-02 14:39:10 +08:00
疯狂的狮子Li 53f857714d add: 新增 ruoyi-common-liteflow 规则引擎模块 2026-07-02 14:37:53 +08:00
Nottyjay 6876bd355f !867 fix 修复 非常规字段生成时htmlType为null导致模板生成失败
* fix 修复 非常规字段生成时htmlType为null导致模板生成失败
2026-07-01 03:02:15 +00:00
疯狂的狮子Li 7ca18b7798 update 重写 s3 授权签名 支持私有库自定义域名 2026-07-01 09:44:15 +08:00
Kayleigh Wang dbbc790081 !865 fix 修正代码生成模块文档扫描包名 org.dromara.generator -> org.dromara.gen
* fix 修正代码生成模块文档扫描包名 org.dromara.generator -> org.dromara.gen
2026-07-01 01:10:04 +00:00
疯狂的狮子Li 4e6dbee008 fix 修复 响应加密编码错误问题 2026-06-30 16:50:59 +08:00
疯狂的狮子Li e571f339b4 update 更新 react 模板适配 2026-06-30 11:36:07 +08:00
YueYe 74ed2734f0 !864 添加lock4j的默认失败策略
* 添加lock4j的默认失败策略
2026-06-30 03:00:03 +00:00
疯狂的狮子Li 1fbe1694e4 fix 修复 检查bean是否存在注解不生效问题 2026-06-29 18:14:35 +08:00
疯狂的狮子Li c56dc6b8e9 update 优化 如果是react前端生成代码 目录使用pages命名 2026-06-29 16:42:40 +08:00
疯狂的狮子Li 1be4f7b07e update snail-ai 升级 1.0.0 正式版 2026-06-29 13:47:57 +08:00
疯狂的狮子Li 5392c3228e fix 修复 单独关闭 openapi client 报错问题 2026-06-26 10:52:12 +08:00
疯狂的狮子Li 837604f889 update 优化 oss 协议头拼接写法 2026-06-26 10:20:15 +08:00
疯狂的狮子Li eab4228548 fix 修复 push 模块 sse/ws 浏览器多标签页互挤掉线问题 2026-06-25 17:23:44 +08:00
疯狂的狮子Li e8b033c4ef fix 修复 代码生成 模板方法调用写法错误 2026-06-25 14:56:33 +08:00
疯狂的狮子Li 3cf2eceb13 fix 修复 fm模板将Long类型格式化出现问题 2026-06-25 09:30:11 +08:00
疯狂的狮子Li 1d263f7656 update 优化 处理一些代码生成模板的一些写法问题 2026-06-24 17:33:04 +08:00
专业坑队 c1e344be0e !863 fix(generator): 优化代码生成器模板和逻辑
* fix(generator): 优化代码生成器模板和逻辑
2026-06-24 09:15:22 +00:00
疯狂的狮子Li 612a965c26 update spring-boot-admin 4.1.1 2026-06-23 13:03:38 +08:00
疯狂的狮子Li d7c525d7f8 update 优化 异常处理 2026-06-23 11:39:58 +08:00
疯狂的狮子Li 93a07e57b8 update 优化 基于流程变量优化退回选择问题 2026-06-22 17:04:14 +08:00
疯狂的狮子Li 642c713f78 update 优化 工作流管理页接口增加权限标识符 避免越权 2026-06-22 17:03:48 +08:00
疯狂的狮子Li 123ba9bfba fix 修复 demo数据导致报错问题 2026-06-22 14:44:02 +08:00
疯狂的狮子Li 66b4840356 update 优化 删除无用配置代码 2026-06-22 14:43:49 +08:00
疯狂的狮子Li 14e2cf16cc fix 修复 springdoc 自定义配置在分组下不生效问题 接口切换为全局接口 全局生效 2026-06-22 11:55:26 +08:00
疯狂的狮子Li f3256442c8 fix 修复 白名单路径无法存储多个 2026-06-22 11:43:38 +08:00
142 changed files with 2546 additions and 1729 deletions
+1 -1
View File
@@ -61,4 +61,4 @@ description: 标准后端 CRUD 专家。用于当前项目中的新增单表 CRU
- BO / VO / Entity 是否职责分离
- 导出、分页、删除前校验是否齐全
- 是否只是 generator 裸产物,如果是要继续补齐项目约定
- 前端 `api/types/index.vue` 如需同步,接口路径、返回结构、日期范围参数要与后端一致
- 前端 `api/types` 和 Vue `index.vue` 或 React `index.tsx` 如需同步,接口路径、返回结构、日期范围参数要与后端一致
+2 -2
View File
@@ -12,12 +12,12 @@ description: 后端工程总入口。用于在当前 RuoYi-Vue-Plus 项目中识
3. 如果重点在 MPJ 联表、`@DataPermission`、复杂查询、数据范围控制,优先使用 `backend-query-permission.md` 的规则。
4. 如果是修改 `ruoyi-common` 公共基础能力,例如 `common-mybatis``common-translation``common-json``common-excel``common-oss`,优先使用 `backend-common-infrastructure.md` 的规则。
5. 如果只要求补充或修正 JavaDoc 注释,优先使用 `backend-javadoc.md` 的规则。
6. 如果同时要求同步前端接口或前端页面骨架,保持后端路由与 generator 风格稳定,便于前端 agent 对接。
6. 如果同时要求同步前端接口或前端页面骨架,先确认目标前端是 Vue 还是 React,保持后端路由与 generator 风格稳定,便于前端 agent 对接。
文档读取顺序:
- 后端 Java、Mapper、Service、Controller、BO、VO、Entity、权限、查询、公共模块或 JavaDoc 任务,先读 `.codex/skills/ruoyi-plus-ai-coding/references/backend.md`
- 同步前端 Vue、TypeScript、api、types 或页面骨架时,再读 `.codex/skills/ruoyi-plus-ai-coding/references/frontend.md`
- 同步前端 Vue、React、TypeScript、api、types 或页面骨架时,再读 `.codex/skills/ruoyi-plus-ai-coding/references/frontend.md`
- 任务边界不清晰或需要标准场景示例时,再读 `.codex/skills/ruoyi-plus-ai-coding/references/examples.md`
- 只读取当前任务相关的 reference,不一次性展开全部文档。
- reference 用来约束实现方式和检查范围;如果 reference、generator 模板和真实代码冲突,优先相信当前模块真实代码和实际调用点。
+5 -5
View File
@@ -1,6 +1,6 @@
---
name: ruoyi-plus-ai-coding
description: 在仓库内按代码生成器模板、项目 reference 文档和既有约定生成或修改代码。用于新增或修改 CRUD 模块、controller/service/mapper/BO/VO/entity、MyBatis-Plus/MPJ 查询、数据权限、缓存、翻译/JSON 增强、公共 common 模块能力、JavaDoc 注释,以及与后端接口配套的 Vue 3 + TypeScript 页面、types 和 api 文件;触发后应先按任务类型读取对应 references,再阅读目标模块真实代码和 generator 模板。
description: 在仓库内按代码生成器模板、项目 reference 文档和既有约定生成或修改代码。用于新增或修改 CRUD 模块、controller/service/mapper/BO/VO/entity、MyBatis-Plus/MPJ 查询、数据权限、缓存、翻译/JSON 增强、公共 common 模块能力、JavaDoc 注释,以及与后端接口配套的 Vue 或 React 前端页面、types 和 api 文件;触发后应先按任务类型读取对应 references,再阅读目标模块真实代码和 generator 模板。
---
# RuoYi Plus AI 编码规范
@@ -17,7 +17,7 @@ description: 在仓库内按代码生成器模板、项目 reference 文档和
- 修改 `ruoyi-common` 公共能力,例如 mybatis 查询构造器、translation、json enhance、excel、oss、redis、web 配置。
- 补充或修正 JavaDoc 注释,尤其是公共 API、接口、BO/VO/Entity 字段、Mapper 默认方法、Service/Controller 方法。
- 在系统、监控、工作流、demo 等模块内按现有约定扩展业务代码。
- 为后端新增接口同步补前端 `api/types/index.vue` 骨架。
- 为后端新增接口同步补前端 `api/types` 和 Vue `index.vue` 或 React `index.tsx` 页面骨架。
## 不适用场景
@@ -45,7 +45,7 @@ description: 在仓库内按代码生成器模板、项目 reference 文档和
使用本 skill 时,先按任务类型读取适用 reference,不一次性展开所有文档:
- 后端 Java、Mapper、Service、Controller、BO、VO、Entity、权限、查询、公共模块或 JavaDoc 任务,先读 [references/backend.md](references/backend.md)。
- 前端 Vue、TypeScript、api、types 或页面任务,先读 [references/frontend.md](references/frontend.md)。
- 前端 Vue、React、TypeScript、api、types 或页面任务,先读 [references/frontend.md](references/frontend.md)。
- 不确定任务边界、需要标准调用方式或需要对照典型场景时,再读 [references/examples.md](references/examples.md)。
reference 用来约束实现方式和自检范围;发生冲突时,仍以当前模块真实代码和实际调用点为准。
@@ -71,7 +71,7 @@ Java、MyBatis-Plus、BO/VO/entity、controller、mapper、service 的具体规
## 前端规则
Vue 3、TypeScript API 文件、生成式列表页、表单状态、字典和日期范围约定见 [references/frontend.md](references/frontend.md)。
Vue 3、React、TypeScript API 文件、生成式列表页、表单状态、字典和日期范围约定见 [references/frontend.md](references/frontend.md)。
## 使用案例
@@ -163,7 +163,7 @@ Vue 3、TypeScript API 文件、生成式列表页、表单状态、字典和日
- BO 使用 `@AutoMapper(target = Entity.class, reverseConvertGenerate = false)`
- VO 使用 `@AutoMapper(target = Entity.class)`
- 前端 API 路径与后端路由完全对应。
- 前端列表页继续使用仓库里的 `proxy?.addDateRange``proxy?.$modal``proxy?.download``useDict``pagination` 等工具
- 前端列表页继续使用对应前端工程已有工具:Vue 侧如 `proxy?.addDateRange``proxy?.$modal``proxy?.download``useDict``pagination`React 侧如 `ProTable``ModalForm``useTableSelection``useDateRangeQuery``useTableExport`
## 推荐提问方式
@@ -62,13 +62,13 @@
### 用户提问示例
```text
使用 $ruoyi-plus-ai-coding 为 monitor/cache 新增一个导出接口,并同步补齐前端 api/types 调用骨架。
使用 $ruoyi-plus-ai-coding 为 monitor/cache 新增一个导出接口,并同步补齐 Vue 或 React 前端 api/types 调用骨架。
```
### 期望执行方式
- 先补后端 `controller/service`
- 再根据后端路由补前端 `src/api` 或 generator 风格的前端骨架。
- 再根据后端路由和目标前端类型`src/api` 或 generator 风格的前端骨架。
- 保证导出接口路径和前端下载调用一致。
## 案例 6:推荐的高质量任务描述
@@ -6,7 +6,10 @@
- 默认 Vue 模板在 `fm/vue`React 模板在 `fm/react`
- 前端工程中与目标模块最接近的现有页面
当前 boot4 仓库通常只含后端与 generator 前端模板;如果前端工程不在当前 root,先以 generator 模板约定为准,再对照用户提供的前端目录或相邻仓库。
当前 boot4 仓库通常只含后端与 generator 前端模板;如果前端工程不在当前仓库根目录,先以 generator 模板约定为准,再对照用户提供的前端工程或官方前端分支:
- Vue 前端:`https://gitee.com/JavaLionLi/plus-ui/tree/6.X-Vue`
- React 前端:`https://gitee.com/JavaLionLi/plus-ui/tree/6.X-React`
## 前端模板选择规则
@@ -17,11 +20,10 @@
## API 文件规则
-`@/utils/request` 引入 `request`
-`@/utils/api-types` 引入 `AxiosPromise`
- `@/api/types` 引入 `PageResult`
- `@/api/<module>/<business>/types` 引入本模块类型
- 列表接口通常返回 `AxiosPromise<PageResult<Vo>>`
- Vue 模板`@/utils/request` 引入 `request`,从 `@/utils/api-types` 引入 `AxiosPromise`,从 `@/api/types` 引入 `PageResult`
- React 模板从 `@/api/request` 引入 `request``@/api/types` 引入 `R``PageResult`
- 本模块类型:Vue 模板从 `@/api/<module>/<business>/types` 引入,React 模板从 `./types` 引入
- Vue 列表接口通常返回 `AxiosPromise<PageResult<Vo>>`React 列表接口通常返回 `request<R<PageResult<Vo>>>(...)`
- 常规接口命名和路由保持:
`listXxx` -> `GET /<module>/<business>/list`
`getXxx` -> `GET /<module>/<business>/{id}`
@@ -38,7 +40,7 @@
- Java 数值类型通常映射为 `number`
- Boolean 映射为 `boolean`
- 其他生成字段默认多为 `string`
- 存在日期范围查询时保留 `params?: any`
- 存在日期范围查询时保留 `params`Vue 模板通常是 `params?: any`React 模板通常是 `params?: Record<string, unknown>`
## Vue 页面规则
@@ -51,7 +53,19 @@
- 弹窗状态优先由 `useFormDialog` 返回的 `dialog``openDialog``showDialog``closeDialog` 管理。
- 表单引用通常命名为 `queryFormRef``<business>FormRef`
## 页面行为规则
## React 页面规则
- 使用 `index.tsx`,组件默认导出 `<BusinessName>Page`
- 页面主体优先沿用 Ant Design Pro`PageContainer``ProTable``ModalForm``ProColumns``ActionType`
- 表单优先使用 `Form.useForm<Form>()`,弹窗开关优先使用 `ahooks``useBoolean`
- 权限通过 `useUserStore``userInfo`,再用 `hasPermi(userInfo, ['module:business:action'])` 生成 `canAdd``canEdit``canRemove``canExport`
- 表格选择使用 `useTableSelection<VO>(row => row.id)`;表格刷新使用 `actionRef.current?.reload()``reloadAndRest?.()`
- 字典使用 `useDict``dictOptions`,展示使用 `DictTag`
- 日期范围使用 `useDateRangeQuery`,在 `ProTable``request` 中由 `toPageQuery(params)` 转查询参数后再应用范围字段。
- 导出使用 `useTableExport`,路径保持 `/<module>/<business>/export`
- 文件、图片、富文本组件使用 React 工程已有的 `FileUpload``ImageUpload``ImagePreview``RichTextEditor`
## Vue 页面行为规则
- `getList` 负责通过 `withLoading` 设置 loading、处理日期范围参数、调用列表接口、回填 `rows``total`
- `handleQuery` 通常先把 `pageNum` 重置为 `1`,再重新查询。
@@ -63,13 +77,20 @@
- `handleDelete` 使用 `modal.confirm(...)` 确认,再调用删除接口并刷新。
- `handleExport` 使用 `download as requestDownload``@/utils/request` 导出的下载方法。
## React 页面行为规则
- React `ProTable` 页面通过 `request` 回调加载列表并返回 `toTableData(res)`;新增、修改、删除成功后调用 `actionRef` 刷新。
- React 弹窗提交函数根据主键判断调用新增还是更新,成功后 `message.success('操作成功')` 并重置表单。
## 模板结构规则
- 优先保持生成器的页面布局结构:搜索区卡片、表格区卡片、工具栏、分页、弹窗表单
- 保留 `v-hasPermi="['module:business:add']"` 这类权限指令。
- 继续使用仓库已有组件:`right-toolbar``pagination``dict-tag``image-preview``image-upload``file-upload``editor`
- 优先保持生成器的页面布局结构,不在 Vue 和 React 之间互相移植组件体系
- Vue 保留 `v-hasPermi="['module:business:add']"` 这类权限指令。
- Vue 继续使用仓库已有组件:`right-toolbar``pagination``dict-tag``image-preview``image-upload``file-upload``editor`
- React 继续使用仓库已有组件:`RowActions``DictTag``ImagePreview``ImageUpload``FileUpload``RichTextEditor`
- 已有页面对时间列使用 `parseTime` 时,新页面保持一致。
- BETWEEN 日期查询继续使用 `el-date-picker`,脚本侧通过 `useDateRangeQuery` 生成 `dateRangeXxx``applyXxxDateRange``resetXxxDateRange`
- Vue BETWEEN 日期查询继续使用 `el-date-picker`,脚本侧通过 `useDateRangeQuery` 生成 `dateRangeXxx``applyXxxDateRange``resetXxxDateRange`
- React BETWEEN 日期查询继续使用 `ProTable``dateTimeRange` 搜索列,查询侧通过 `useDateRangeQuery` 写入 `params`
## 避免事项
@@ -77,3 +98,4 @@
- 模块已使用字典时,不要把选项文案硬编码到页面里。
- 不要让 API 函数名和路由段偏离后端约定。
- 后端 BO/service 依赖 begin/end 参数时,不要从查询对象里删掉 `params` 和日期范围处理。
- 不要把 Vue 的 `proxy``v-hasPermi`、Element Plus 组件写进 React 页面,也不要把 React 的 `ProTable``ModalForm`、Ant Design 权限判断写进 Vue 页面。
+2 -1
View File
@@ -22,7 +22,8 @@
> 系统演示: [传送门](https://plus-doc.dromara.org/#/common/demo_system)
> 官方前端项目地址: [gitee](https://gitee.com/JavaLionLi/plus-ui) - [github](https://github.com/CrazyLionCat/plus-ui) - [gitcode](https://gitcode.com/dromara/plus-ui)<br>
> 官方前端项目地址: 基于 Vue + ElementPlus 版本前端项目 [gitee](https://gitee.com/JavaLionLi/plus-ui) - [github](https://github.com/CrazyLionCat/plus-ui) - [gitcode](https://gitcode.com/dromara/plus-ui)<br>
> 官方前端项目地址: 基于 React + Ant Design 版本前端项目 [plus-ui-react](https://gitee.com/JavaLionLi/plus-ui/tree/6.X-React/)
> 成员前端项目地址: 基于vben5 [ruoyi-plus-vben5](https://github.com/imdap/ruoyi-plus-vben5)<br>
> 成员前端项目地址: 基于soybean [ruoyi-plus-soybean](https://gitee.com/xlsea/ruoyi-plus-soybean)<br>
> 成员项目地址: 删除多租户与工作流 [RuoYi-Vue-Plus-Single](https://gitee.com/ColorDreams/RuoYi-Vue-Plus-Single)<br>
+25 -17
View File
@@ -14,21 +14,21 @@
<properties>
<!-- 项目基础配置 -->
<revision>6.0.0-BETA</revision>
<revision>6.0.0</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>21</java.version>
<!-- Spring 生态依赖版本 -->
<spring-boot.version>4.1.0</spring-boot.version>
<spring-boot-admin.version>4.1.0</spring-boot-admin.version>
<spring-boot-admin.version>4.1.2</spring-boot-admin.version>
<spring-ai.version>2.0.0</spring-ai.version>
<springdoc.version>3.0.3</springdoc.version>
<!-- 数据访问与持久化相关依赖版本 -->
<mybatis.version>3.5.19</mybatis.version>
<mybatis-plus.version>3.5.16</mybatis-plus.version>
<mybatis-plus-join.version>1.5.7</mybatis-plus-join.version>
<mybatis-plus.version>3.5.17</mybatis-plus.version>
<mybatis-plus-join.version>1.5.9</mybatis-plus-join.version>
<dynamic-ds.version>4.5.0</dynamic-ds.version>
<anyline.version>8.7.3-20260306</anyline.version>
<easy-es.version>3.0.2</easy-es.version>
@@ -36,35 +36,36 @@
<!-- 安全认证与加密相关依赖版本 -->
<satoken.version>1.45.0</satoken.version>
<justauth.version>1.16.7</justauth.version>
<bouncycastle.version>1.84</bouncycastle.version>
<justauth.version>3.0.1</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.1.2</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>
@@ -348,7 +349,7 @@
<!-- Warm-Flow国产工作流引擎, 在线文档:http://warm-flow.cn/ -->
<dependency>
<groupId>org.dromara.warm</groupId>
<artifactId>warm-flow-mybatis-plus-sb3-starter</artifactId>
<artifactId>warm-flow-mybatis-plus-sb4-starter</artifactId>
<version>${warm-flow.version}</version>
</dependency>
<!-- Warm-Flow UI 插件 -->
@@ -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>io.github.windtool</groupId>
<artifactId>JustAuth</artifactId>
<version>${justauth.version}</version>
</dependency>
+2 -2
View File
@@ -1,6 +1,6 @@
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
FROM bellsoft/liberica-openjdk-rocky:21.0.11-cds
# FROM bellsoft/liberica-openjdk-rocky:25.0.3-cds
FROM bellsoft/liberica-openjdk-rocky:21.0.12-cds
# FROM bellsoft/liberica-openjdk-rocky:25.0.4-cds
# FROM findepi/graalvm:java21-native
LABEL maintainer="Lion Li"
+4
View File
@@ -143,6 +143,10 @@
<artifactId>ruoyi-gen</artifactId>
</dependency>
</dependencies>
<activation>
<!-- 默认激活 -->
<activeByDefault>true</activeByDefault>
</activation>
</profile>
</profiles>
@@ -7,6 +7,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Opt;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.lock.annotation.Lock4j;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthUser;
@@ -136,7 +137,12 @@ public class SysLoginService {
loginInfoEvent.setUsername(username);
loginInfoEvent.setStatus(status);
loginInfoEvent.setMessage(message);
loginInfoEvent.setRequest(ServletUtils.getRequest());
HttpServletRequest request = ServletUtils.getRequest();
if (request != null) {
loginInfoEvent.setIp(ServletUtils.getClientIP(request));
loginInfoEvent.setUserAgent(request.getHeader("User-Agent"));
loginInfoEvent.setClientId(request.getHeader(LoginHelper.CLIENT_KEY));
}
SpringUtils.context().publishEvent(loginInfoEvent);
}
@@ -1,6 +1,7 @@
package org.dromara.web.service;
import cn.hutool.crypto.digest.BCrypt;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.GlobalConstants;
@@ -14,6 +15,7 @@ import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.log.event.LoginInfoEvent;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.web.config.properties.CaptchaProperties;
import org.dromara.system.api.model.RegisterBody;
import org.dromara.system.domain.SysUser;
@@ -103,7 +105,12 @@ public class SysRegisterService {
loginInfoEvent.setUsername(username);
loginInfoEvent.setStatus(status);
loginInfoEvent.setMessage(message);
loginInfoEvent.setRequest(ServletUtils.getRequest());
HttpServletRequest request = ServletUtils.getRequest();
if (request != null) {
loginInfoEvent.setIp(ServletUtils.getClientIP(request));
loginInfoEvent.setUserAgent(request.getHeader("User-Agent"));
loginInfoEvent.setClientId(request.getHeader(LoginHelper.CLIENT_KEY));
}
SpringUtils.context().publishEvent(loginInfoEvent);
}
@@ -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
# 是否锁定当前智能体
+13 -1
View File
@@ -182,7 +182,7 @@ springdoc:
- group: 3.系统模块
packages-to-scan: org.dromara.system
- group: 4.代码生成模块
packages-to-scan: org.dromara.generator
packages-to-scan: org.dromara.gen
- group: 5.工作流模块
packages-to-scan: org.dromara.workflow
@@ -245,6 +245,18 @@ warm-flow:
# 默认Authorization,如果有多个token,用逗号分隔
token-name: ${sa-token.token-name},clientid
--- # LiteFlow 业务规则编排
liteflow:
# 跟随工作流模块开关,避免禁用工作流时仍加载任务办理链
enable: ${warm-flow.enabled:true}
metrics:
# 指标自动配置使用独立开关,需同步关闭以避免初始化未启用的 LiteFlow
enabled: ${liteflow.enable:true}
# 工作流模块用于编排任务办理链路
rule-source: classpath:liteflow/*.el.xml
# 开启虚拟线程
enable-virtual-thread: true
--- # mqtt 配置
# 具体配置还需查看文档
# https://mica-mqtt.dreamlu.net/guide/spring/client.html
+1
View File
@@ -37,6 +37,7 @@
<module>ruoyi-common-json</module>
<module>ruoyi-common-encrypt</module>
<module>ruoyi-common-push</module>
<module>ruoyi-common-liteflow</module>
<module>ruoyi-common-mqtt</module>
<module>ruoyi-common-ai</module>
<module>ruoyi-common-mcp</module>
@@ -1,124 +0,0 @@
package org.dromara.common.ai.handler;
import com.aizuda.snail.ai.agent.chat.starter.SnailAiChatGatewayController;
import com.aizuda.snail.ai.common.execption.BaseSnailAiException;
import com.aizuda.snail.ai.common.execption.SnailAiAuthenticationException;
import com.aizuda.snail.ai.common.model.ModelCallException;
import com.aizuda.snail.ai.common.model.Result;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.HandlerMethodValidationException;
import java.util.Collection;
import java.util.stream.Collectors;
/**
* Snail AI Chat 网关异常处理器。
* <p>
* /api/snail/chat/** 接口由 Snail AI SDK 前端消费,响应结构需要保持 SDK Result(status/message/data)
* 不能落到 RuoYi 通用 R(code/msg/data) 格式。
*/
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
@RestControllerAdvice(assignableTypes = SnailAiChatGatewayController.class)
public class SnailAiChatExceptionHandler {
private static final int AUTHENTICATION_ERROR_STATUS = 5001;
@ExceptionHandler(SnailAiAuthenticationException.class)
public Result<Void> handleAuthenticationException(SnailAiAuthenticationException e) {
log.warn("Snail AI Chat authentication failed: {}", e.getMessage());
return Result.fail(AUTHENTICATION_ERROR_STATUS, defaultMessage(e, "认证失败,请重新登录"));
}
@ExceptionHandler(BaseSnailAiException.class)
public Result<Void> handleSnailAiException(BaseSnailAiException e) {
log.warn("Snail AI Chat request failed: {}", e.getMessage());
return Result.fail(defaultMessage(e, "AI 服务请求失败,请稍后再试"));
}
@ExceptionHandler(ModelCallException.class)
public Result<Void> handleModelCallException(ModelCallException e) {
log.warn("Snail AI Chat model call failed: {}", e.getMessage(), e);
return Result.fail(defaultMessage(e, "模型调用失败,请稍后再试"));
}
@ExceptionHandler({
BindException.class,
MethodArgumentNotValidException.class,
HandlerMethodValidationException.class
})
public Result<Void> handleValidationException(Exception e) {
log.warn("Snail AI Chat validation failed: {}", e.getMessage());
return Result.fail(validationMessage(e));
}
@ExceptionHandler(ConstraintViolationException.class)
public Result<Void> handleConstraintViolationException(ConstraintViolationException e) {
log.warn("Snail AI Chat constraint validation failed: {}", e.getMessage());
String message = e.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.filter(this::hasText)
.collect(Collectors.joining(", "));
return Result.fail(hasText(message) ? message : "请求参数校验失败");
}
@ExceptionHandler(HttpMessageNotReadableException.class)
public Result<Void> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
log.warn("Snail AI Chat request body parse failed: {}", e.getMessage());
return Result.fail("请求参数格式错误");
}
@ExceptionHandler(IllegalArgumentException.class)
public Result<Void> handleIllegalArgumentException(IllegalArgumentException e) {
log.warn("Snail AI Chat illegal argument: {}", e.getMessage());
return Result.fail(defaultMessage(e, "请求参数不合法"));
}
@ExceptionHandler(IllegalStateException.class)
public Result<Void> handleIllegalStateException(IllegalStateException e) {
log.warn("Snail AI Chat illegal state: {}", e.getMessage());
return Result.fail(defaultMessage(e, "AI 会话状态异常,请刷新后重试"));
}
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("Snail AI Chat unexpected exception", e);
return Result.fail("AI 服务异常,请稍后再试");
}
private String validationMessage(Exception e) {
Collection<? extends MessageSourceResolvable> errors;
if (e instanceof BindException bindException) {
errors = bindException.getAllErrors();
} else if (e instanceof MethodArgumentNotValidException methodArgumentNotValidException) {
errors = methodArgumentNotValidException.getBindingResult().getAllErrors();
} else if (e instanceof HandlerMethodValidationException handlerMethodValidationException) {
errors = handlerMethodValidationException.getAllErrors();
} else {
return "请求参数校验失败";
}
String message = errors.stream()
.map(MessageSourceResolvable::getDefaultMessage)
.filter(this::hasText)
.collect(Collectors.joining(", "));
return hasText(message) ? message : "请求参数校验失败";
}
private String defaultMessage(Throwable e, String fallback) {
return hasText(e.getMessage()) ? e.getMessage() : fallback;
}
private boolean hasText(String value) {
return value != null && !value.isBlank();
}
}
+8 -1
View File
@@ -14,7 +14,7 @@
</description>
<properties>
<revision>6.0.0-BETA</revision>
<revision>6.0.0</revision>
</properties>
<dependencyManagement>
@@ -159,6 +159,13 @@
<version>${revision}</version>
</dependency>
<!-- liteflow模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-liteflow</artifactId>
<version>${revision}</version>
</dependency>
<!-- mqtt模块 -->
<dependency>
<groupId>org.dromara</groupId>
@@ -12,7 +12,7 @@ import org.dromara.common.doc.core.customizer.JavadocOperationCustomizer;
import org.dromara.common.doc.core.resolver.JavadocResolver;
import org.dromara.common.doc.core.resolver.SaTokenAnnotationMetadataJavadocResolver;
import org.springdoc.core.configuration.SpringDocConfiguration;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springdoc.core.providers.JavadocProvider;
import org.springdoc.core.utils.PropertyResolverUtils;
import org.springframework.boot.autoconfigure.AutoConfiguration;
@@ -108,7 +108,7 @@ public class SpringDocConfig {
* 对已经生成好的 OpenApi 进行自定义操作
*/
@Bean
public OpenApiCustomizer openApiCustomizer() {
public GlobalOpenApiCustomizer openApiCustomizer() {
String contextPath = serverProperties.getServlet().getContextPath();
String finalContextPath;
if (StringUtils.isBlank(contextPath) || "/".equals(contextPath)) {
@@ -8,8 +8,8 @@ import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springdoc.core.customizers.GlobalOperationCustomizer;
import org.springdoc.core.providers.JavadocProvider;
import org.springdoc.core.service.OpenAPIService;
import org.springdoc.core.utils.PropertyResolverUtils;
@@ -29,7 +29,7 @@ import java.util.stream.Stream;
* @author Lion Li
*/
@RequiredArgsConstructor
public class ClassTagOperationCustomizer implements OperationCustomizer, OpenApiCustomizer {
public class ClassTagOperationCustomizer implements GlobalOperationCustomizer, GlobalOpenApiCustomizer {
/**
* JavaDoc 提供器。
@@ -4,7 +4,7 @@ import io.swagger.v3.oas.models.Operation;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.dromara.common.doc.core.resolver.JavadocResolver;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springdoc.core.customizers.GlobalOperationCustomizer;
import org.springdoc.core.providers.JavadocProvider;
import org.springframework.util.CollectionUtils;
import org.springframework.web.method.HandlerMethod;
@@ -18,7 +18,7 @@ import java.util.Optional;
* @author Lion Li
*/
@RequiredArgsConstructor
public class JavadocOperationCustomizer implements OperationCustomizer {
public class JavadocOperationCustomizer implements GlobalOperationCustomizer {
/**
* JavaDoc 提供器。
@@ -23,11 +23,11 @@ import java.util.Base64;
public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
private static final Charset RESPONSE_CHARSET = StandardCharsets.UTF_8;
private final ByteArrayOutputStream byteArrayOutputStream;
private final ServletOutputStream servletOutputStream;
private PrintWriter printWriter;
private Charset charset;
/**
* 构造加密响应包装器。
@@ -39,7 +39,6 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
super(response);
this.byteArrayOutputStream = new ByteArrayOutputStream();
this.servletOutputStream = this.getOutputStream();
this.charset = resolveCharset(response);
}
/**
@@ -50,8 +49,7 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
@Override
public PrintWriter getWriter() {
if (printWriter == null) {
charset = resolveCharset((HttpServletResponse) getResponse());
printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream, charset));
printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream, RESPONSE_CHARSET));
}
return printWriter;
}
@@ -106,7 +104,7 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
*/
public String getContent() throws IOException {
flushBuffer();
return byteArrayOutputStream.toString(charset);
return byteArrayOutputStream.toString(RESPONSE_CHARSET);
}
/**
@@ -130,14 +128,14 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
// vue版本需要设置
servletResponse.addHeader("Access-Control-Expose-Headers", headerFlag);
servletResponse.setHeader(headerFlag, encryptPassword);
servletResponse.setCharacterEncoding(charset.name());
servletResponse.setCharacterEncoding(RESPONSE_CHARSET.name());
// 获取原始内容
String originalBody = this.getContent();
// 对内容进行加密
String encryptContent = EncryptUtils.encryptByAes(originalBody, aesPassword);
servletResponse.setContentLengthLong(encryptContent.getBytes(charset).length);
servletResponse.setContentLengthLong(encryptContent.getBytes(RESPONSE_CHARSET).length);
return encryptContent;
}
@@ -206,20 +204,6 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
};
}
/**
* 解析响应字符集,未设置时默认使用 UTF-8。
*
* @param response 原始响应
* @return 响应字符集
*/
private Charset resolveCharset(HttpServletResponse response) {
String characterEncoding = response.getCharacterEncoding();
if (characterEncoding == null) {
return StandardCharsets.UTF_8;
}
return Charset.forName(characterEncoding);
}
/**
* 生成响应内容 AES 加密密钥。
*
@@ -1,11 +1,14 @@
package org.dromara.common.encrypt.interceptor;
import lombok.AllArgsConstructor;
import org.apache.ibatis.cursor.Cursor;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import org.dromara.common.encrypt.core.EncryptedFieldProcessor;
import java.io.IOException;
import java.sql.Statement;
import java.util.Iterator;
import java.util.Properties;
/**
@@ -13,10 +16,10 @@ import java.util.Properties;
*
* @author Lion Li
*/
@Intercepts({@Signature(
type = ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class})
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}),
// MP 3.5.17 起 selectOne/selectVoOne 走游标,需一并拦截解密
@Signature(type = ResultSetHandler.class, method = "handleCursorResultSets", args = {Statement.class})
})
@AllArgsConstructor
public class MybatisDecryptInterceptor implements Interceptor {
@@ -32,11 +35,13 @@ public class MybatisDecryptInterceptor implements Interceptor {
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取执行mysql执行结果
Object result = invocation.proceed();
if (result == null) {
return null;
}
if (result instanceof Cursor<?> cursor) {
return new DecryptCursor<>(cursor, encryptedFieldProcessor);
}
encryptedFieldProcessor.decrypt(result);
return result;
}
@@ -61,4 +66,52 @@ public class MybatisDecryptInterceptor implements Interceptor {
public void setProperties(Properties properties) {
}
/**
* 查询结果解密游标,保持 MyBatis 游标的惰性读取语义。
*
* @param <T> 查询结果类型
*/
private record DecryptCursor<T>(Cursor<T> delegate, EncryptedFieldProcessor encryptedFieldProcessor)
implements Cursor<T> {
@Override
public boolean isOpen() {
return delegate.isOpen();
}
@Override
public boolean isConsumed() {
return delegate.isConsumed();
}
@Override
public int getCurrentIndex() {
return delegate.getCurrentIndex();
}
@Override
public Iterator<T> iterator() {
Iterator<T> iterator = delegate.iterator();
return new Iterator<>() {
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public T next() {
T result = iterator.next();
encryptedFieldProcessor.decrypt(result);
return result;
}
};
}
@Override
public void close() throws IOException {
delegate.close();
}
}
}
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common-liteflow</artifactId>
<description>
ruoyi-common-liteflow LiteFlow规则编排模块
</description>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<!-- LiteFlow 规则编排 -->
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,19 @@
package org.dromara.common.liteflow.component;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeBooleanComponent;
/**
* LiteFlow 恒为 false 的条件节点。
*
* @author Lion Li
*/
@LiteflowComponent("alwaysFalse")
public class AlwaysFalseComponent extends NodeBooleanComponent {
@Override
public boolean processBoolean() {
return false;
}
}
@@ -0,0 +1,19 @@
package org.dromara.common.liteflow.component;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeBooleanComponent;
/**
* LiteFlow 恒为 true 的条件节点。
*
* @author Lion Li
*/
@LiteflowComponent("alwaysTrue")
public class AlwaysTrueComponent extends NodeBooleanComponent {
@Override
public boolean processBoolean() {
return true;
}
}
@@ -0,0 +1,22 @@
package org.dromara.common.liteflow.component;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import org.dromara.common.core.exception.ServiceException;
/**
* LiteFlow 上下文必填校验节点。
*
* @author Lion Li
*/
@LiteflowComponent("contextRequired")
public class ContextRequiredComponent extends NodeComponent {
@Override
public void process() {
if (getFirstContextBean() == null) {
throw new ServiceException("LiteFlow 上下文不能为空");
}
}
}
@@ -0,0 +1,40 @@
package org.dromara.common.liteflow.component;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.liteflow.core.FailMessageProvider;
/**
* LiteFlow 失败节点,用于显式表达链路分支不可继续执行。
*
* @author Lion Li
*/
@LiteflowComponent("fail")
public class FailComponent extends NodeComponent {
private static final String DEFAULT_MESSAGE = "LiteFlow 链路执行失败";
@Override
public void process() {
throw new ServiceException(getFailMessage());
}
/**
* 优先从上下文读取业务失败提示,未提供时使用默认提示。
*
* @return 失败提示
*/
private String getFailMessage() {
Object context = getFirstContextBean();
if (context instanceof FailMessageProvider provider) {
String message = provider.getFailMessage();
if (StringUtils.isNotBlank(message)) {
return message;
}
}
return DEFAULT_MESSAGE;
}
}
@@ -0,0 +1,19 @@
package org.dromara.common.liteflow.component;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
/**
* LiteFlow 空节点,用于显式表达无需处理的分支。
*
* @author Lion Li
*/
@LiteflowComponent("noop")
public class NoopComponent extends NodeComponent {
@Override
public void process() {
// no-op
}
}
@@ -0,0 +1,67 @@
package org.dromara.common.liteflow.config;
import org.dromara.common.liteflow.component.*;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
/**
* LiteFlow 公共节点自动配置。
*
* @author Lion Li
*/
@AutoConfiguration
@ConditionalOnProperty(value = "liteflow.enable", havingValue = "true")
public class LiteFlowAutoConfiguration {
/**
* 空节点,用于显式表达无需处理的分支。
*
* @return 空节点
*/
@Bean("noop")
public NoopComponent noopComponent() {
return new NoopComponent();
}
/**
* 失败节点,用于显式抛出业务失败提示。
*
* @return 失败节点
*/
@Bean("fail")
public FailComponent failComponent() {
return new FailComponent();
}
/**
* 上下文必填校验节点。
*
* @return 上下文必填校验节点
*/
@Bean("contextRequired")
public ContextRequiredComponent contextRequiredComponent() {
return new ContextRequiredComponent();
}
/**
* 恒为 true 的条件节点。
*
* @return true 条件节点
*/
@Bean("alwaysTrue")
public AlwaysTrueComponent alwaysTrueComponent() {
return new AlwaysTrueComponent();
}
/**
* 恒为 false 的条件节点。
*
* @return false 条件节点
*/
@Bean("alwaysFalse")
public AlwaysFalseComponent alwaysFalseComponent() {
return new AlwaysFalseComponent();
}
}
@@ -0,0 +1,17 @@
package org.dromara.common.liteflow.core;
/**
* LiteFlow 失败提示提供者。
*
* @author Lion Li
*/
public interface FailMessageProvider {
/**
* 获取公共失败节点抛出的业务提示。
*
* @return 失败提示
*/
String getFailMessage();
}
@@ -0,0 +1,42 @@
package org.dromara.common.liteflow.utils;
import com.yomahub.liteflow.core.FlowExecutor;
import com.yomahub.liteflow.flow.LiteflowResponse;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.SpringUtils;
/**
* LiteFlow 执行工具。
*
* @author Lion Li
*/
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class LiteFlowUtils {
/**
* 执行 LiteFlow 链路,并按 contextBean 语义透传业务上下文和失败原因。
*
* @param chainId 链路标识
* @param context 链路上下文
*/
public static void execute(String chainId, Object context) {
if (context == null) {
throw new ServiceException("LiteFlow 上下文不能为空");
}
LiteflowResponse response = SpringUtils.getBean(FlowExecutor.class).execute2Resp(chainId, null, context);
if (!response.isSuccess()) {
Exception cause = response.getCause();
log.error("LiteFlow 链路执行失败 chainId={} requestId={} message={} steps={}",
chainId, response.getRequestId(), response.getMessage(), response.getExecuteStepStrWithTime(), cause);
if (cause instanceof RuntimeException runtimeException) {
throw runtimeException;
}
throw new ServiceException(cause != null ? cause.getMessage() : response.getMessage());
}
}
}
@@ -0,0 +1 @@
org.dromara.common.liteflow.config.LiteFlowAutoConfiguration
@@ -1,6 +1,5 @@
package org.dromara.common.log.event;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Data;
import java.io.Serial;
@@ -34,9 +33,19 @@ public class LoginInfoEvent implements Serializable {
private String message;
/**
* 请求体
* 客户端IP
*/
private HttpServletRequest request;
private String ip;
/**
* 用户代理
*/
private String userAgent;
/**
* 客户端标识
*/
private String clientId;
/**
* 其他参数
@@ -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;
}
@@ -26,14 +26,19 @@ public class DataPermissionAdvice implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
Object target = invocation.getThis();
Method method = invocation.getMethod();
DataPermission previousPermission = DataPermissionHelper.getPermission();
// 设置权限注解
DataPermissionHelper.setPermission(getDataPermissionAnnotation(target, method));
try {
// 执行代理方法
return invocation.proceed();
} finally {
// 清除权限注解
DataPermissionHelper.removePermission();
// 恢复上一层权限上下文,兼容嵌套 Mapper 调用
if (previousPermission == null) {
DataPermissionHelper.removePermission();
} else {
DataPermissionHelper.setPermission(previousPermission);
}
}
}
@@ -6,7 +6,9 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.MapperProxyMetadata;
import com.baomidou.mybatisplus.core.toolkit.reflect.GenericTypeUtils;
import com.baomidou.mybatisplus.core.toolkit.MybatisUtils;
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.toolkit.ChainWrappers;
@@ -14,6 +16,7 @@ import com.baomidou.mybatisplus.extension.toolkit.Db;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StreamUtils;
import java.io.Serializable;
@@ -100,7 +103,26 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
* @return Lambda CRUD 链式包装器
*/
default LambdaCrudChainWrapper<T, V> lambda() {
return new LambdaCrudChainWrapper<>(this);
return new LambdaCrudChainWrapper<>(mapperProxy());
}
/**
* 获取当前 Mapper 对应的 Spring 代理对象,确保 Mapper 上的切面注解能够继续生效。
*
* @return Mapper 代理对象
*/
private BaseMapperPlus<T, V> mapperProxy() {
BaseMapperPlus<T, V> mapper = this;
try {
MapperProxyMetadata metadata = MybatisUtils.getMapperProxy(this);
Object proxy = SpringUtils.getBean(metadata.getMapperInterface());
if (proxy instanceof BaseMapperPlus<?, ?>) {
mapper = (BaseMapperPlus<T, V>) proxy;
}
} catch (RuntimeException ignored) {
// Mapper 可能未托管于 Spring 容器,保留当前对象作为回退。
}
return mapper;
}
/**
@@ -109,7 +131,7 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
* @return Lambda 链式更新包装器
*/
default LambdaUpdateChainWrapper<T> lambdaUpdate() {
return ChainWrappers.lambdaUpdateChain(this);
return ChainWrappers.lambdaUpdateChain(mapperProxy());
}
/**
@@ -49,6 +49,44 @@ public final class LambdaJoinQueryBuilder<T> {
return this;
}
/**
* 添加分组字段。
*
* @param columns 分组字段
* @param <E> 字段所属实体类型
* @return 当前联表查询构造辅助对象
*/
@SafeVarargs
public final <E> LambdaJoinQueryBuilder<T> groupBy(SFunction<E, ?>... columns) {
if (columns == null) {
return this;
}
for (SFunction<E, ?> column : columns) {
wrapper.groupBy(column);
}
return this;
}
/**
* 添加带表别名的分组字段。
*
* @param tableAlias 表别名
* @param columns 分组字段
* @param <E> 字段所属实体类型
* @return 当前联表查询构造辅助对象
*/
@SafeVarargs
public final <E> LambdaJoinQueryBuilder<T> groupBy(String tableAlias, SFunction<E, ?>... columns) {
if (columns == null || columns.length == 0) {
return this;
}
String checkedAlias = AggregateSelectUtils.checkAlias(tableAlias);
for (SFunction<E, ?> column : columns) {
wrapper.groupBy(checkedAlias, column);
}
return this;
}
/**
* 指定主表查询字段。
*
@@ -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);
@@ -71,13 +71,17 @@ public class PlusDataPermissionHandler {
*/
public Expression getSqlSegment(Expression where, boolean isSelect) {
try {
DataPermission dataPermission = getDataPermission();
if (dataPermission == null) {
throw new ServiceException("数据权限上下文缺失,请检查 @DataPermission 注解处理生命周期");
}
LoginUser currentUser = currentUser();
// 如果是超级管理员或租户管理员,则不过滤数据
if (LoginHelper.isSuperAdmin()) {
return where;
}
// 构造数据过滤条件的 SQL 片段
String dataFilterSql = buildDataFilter(getDataPermission(), currentUser, isSelect);
String dataFilterSql = buildDataFilter(dataPermission, currentUser, isSelect);
if (StringUtils.isBlank(dataFilterSql)) {
return where;
}
@@ -91,8 +95,6 @@ public class PlusDataPermissionHandler {
}
} catch (JSQLParserException e) {
throw new ServiceException("数据权限解析异常 => " + e.getMessage());
} finally {
DataPermissionHelper.removePermission();
}
}
@@ -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;
}
}
@@ -24,13 +24,12 @@ import java.sql.Statement;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.TemporalAccessor;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.locks.ReentrantLock;
/**
* 完整 SQL 日志拦截器。
@@ -71,11 +70,6 @@ public class SqlLogInterceptor implements Interceptor {
*/
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
/**
* 控制台输出锁,避免多线程 SQL 日志互相穿插。
*/
private static final ReentrantLock CONSOLE_LOCK = new ReentrantLock();
/**
* SQL 日志配置。
*/
@@ -340,13 +334,7 @@ public class SqlLogInterceptor implements Interceptor {
* @param message 日志内容
*/
private void printConsole(String message) {
CONSOLE_LOCK.lock();
try {
System.err.println(message);
System.err.println();
} finally {
CONSOLE_LOCK.unlock();
}
System.err.print(message + System.lineSeparator() + System.lineSeparator());
}
/**
@@ -15,6 +15,14 @@ import software.amazon.awssdk.core.ResponseInputStream;
import software.amazon.awssdk.core.async.AsyncRequestBody;
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
import software.amazon.awssdk.core.async.ResponsePublisher;
import software.amazon.awssdk.http.SdkHttpFullRequest;
import software.amazon.awssdk.http.SdkHttpMethod;
import software.amazon.awssdk.http.auth.aws.signer.AwsV4FamilyHttpSigner;
import software.amazon.awssdk.http.auth.aws.signer.AwsV4HttpSigner;
import software.amazon.awssdk.http.auth.spi.signer.HttpSigner;
import software.amazon.awssdk.http.auth.spi.signer.SignRequest;
import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
@@ -25,6 +33,7 @@ import software.amazon.awssdk.transfer.s3.S3TransferManager;
import software.amazon.awssdk.transfer.s3.model.CompletedUpload;
import software.amazon.awssdk.transfer.s3.model.DownloadRequest;
import software.amazon.awssdk.transfer.s3.progress.TransferListener;
import software.amazon.awssdk.utils.http.SdkHttpUtils;
import java.io.*;
import java.nio.channels.Channels;
@@ -33,8 +42,11 @@ import java.nio.channels.SeekableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.net.URI;
import java.util.*;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
@@ -716,6 +728,9 @@ public abstract class AbstractOssClientImpl implements OssClient {
*/
@Override
public String bucketPresignGetUrl(String bucket, String key, Duration expiredTime) {
if (useBucketBoundDomain(bucket)) {
return bucketBoundDomainPresignUrl(SdkHttpMethod.GET, key, expiredTime, Collections.emptyMap());
}
try {
return s3Presigner.presignGetObject(getObjectPresignRequestBuilder -> {
getObjectPresignRequestBuilder.signatureDuration(expiredTime)
@@ -739,6 +754,9 @@ public abstract class AbstractOssClientImpl implements OssClient {
*/
@Override
public String bucketPresignPutUrl(String bucket, String key, Duration expiredTime, Map<String, String> metadata) {
if (useBucketBoundDomain(bucket)) {
return bucketBoundDomainPresignUrl(SdkHttpMethod.PUT, key, expiredTime, metadata);
}
try {
return s3Presigner.presignPutObject(putObjectPresignRequestBuilder -> {
putObjectPresignRequestBuilder.signatureDuration(expiredTime)
@@ -751,6 +769,94 @@ public abstract class AbstractOssClientImpl implements OssClient {
}
}
/**
* 是否使用已绑定默认桶的自定义域名生成无桶名预签名 URL。
*
* @param bucket 存储桶名称
* @return 是否使用自定义域名签名
*/
private boolean useBucketBoundDomain(String bucket) {
return config.domain()
.filter(StringUtils::isNotBlank)
.isPresent() && config.bucket()
.filter(defaultBucket -> Objects.equals(defaultBucket, bucket))
.isPresent();
}
/**
* 使用绑定默认桶的自定义域名生成预签名 URL,避免 S3 SDK 自动拼接桶名。
*
* @param method HTTP 方法
* @param key 对象键
* @param expiredTime 过期时间
* @param metadata 对象元数据
* @return 预签名 URL
*/
private String bucketBoundDomainPresignUrl(SdkHttpMethod method, String key, Duration expiredTime, Map<String, String> metadata) {
try {
URI domainUri = URI.create(config.getDomainUrl());
SdkHttpFullRequest.Builder requestBuilder = SdkHttpFullRequest.builder()
.method(method)
.uri(domainUri)
.encodedPath(bucketBoundDomainPath(domainUri, key));
if (metadata != null) {
metadata.forEach((metadataKey, metadataValue) -> {
if (StringUtils.isNotBlank(metadataKey)) {
requestBuilder.putHeader("x-amz-meta-" + metadataKey, String.valueOf(metadataValue));
}
});
}
AwsCredentialsIdentity credentials = AwsCredentialsIdentity.create(
config.accessKey()
.filter(StringUtils::isNotBlank)
.orElseThrow(() -> S3StorageException.form("accessKey is not configured.")),
config.secretKey()
.filter(StringUtils::isNotBlank)
.orElseThrow(() -> S3StorageException.form("secretKey is not configured."))
);
Clock signingClock = Clock.fixed(Instant.now(), ZoneOffset.UTC);
return AwsV4HttpSigner.create()
.sign(SignRequest.builder(credentials)
.request(requestBuilder.build())
.putProperty(AwsV4HttpSigner.REGION_NAME, config.region().orElse(Region.US_EAST_1).id())
.putProperty(AwsV4FamilyHttpSigner.SERVICE_SIGNING_NAME, "s3")
.putProperty(AwsV4FamilyHttpSigner.AUTH_LOCATION, AwsV4FamilyHttpSigner.AuthLocation.QUERY_STRING)
.putProperty(AwsV4FamilyHttpSigner.PAYLOAD_SIGNING_ENABLED, false)
.putProperty(AwsV4FamilyHttpSigner.EXPIRATION_DURATION, expiredTime)
.putProperty(HttpSigner.SIGNING_CLOCK, signingClock)
.putProperty(AwsV4FamilyHttpSigner.DOUBLE_URL_ENCODE, false)
.putProperty(AwsV4FamilyHttpSigner.NORMALIZE_PATH, false)
.build())
.request()
.getUri()
.toString();
} catch (Exception e) {
throw toStorageException(e);
}
}
/**
* 构建绑定桶域名下的对象访问路径。
*
* @param domainUri 自定义域名 URI
* @param key 对象键
* @return 编码后的访问路径
*/
private String bucketBoundDomainPath(URI domainUri, String key) {
String basePath = Optional.ofNullable(domainUri.getRawPath())
.filter(StringUtils::isNotBlank)
.filter(path -> !"/".equals(path))
.orElse("");
String objectPath = SdkHttpUtils.urlEncodeIgnoreSlashes(key);
if (!basePath.startsWith("/")) {
basePath = "/" + basePath;
}
if (!basePath.endsWith("/")) {
basePath = basePath + "/";
}
return basePath + objectPath;
}
/**
* 上传本地路径文件到默认存储桶。
*
@@ -1,6 +1,5 @@
package org.dromara.common.oss.config;
import cn.hutool.http.HttpUtil;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
@@ -196,8 +195,8 @@ public class OssClientConfig implements Config<OssClientConfig, OssClientConfig.
public String getDomainUrl() {
return domain()
// 如果已经配置了自定义域名,则优先使用域名
// 检查携带协议头
.filter(OssClientConfig::hasHttpHeader)
.filter(StringUtils::isNotBlank)
.map(domain -> BucketUrlUtil.rebuildUrlHeader(useHttps, domain.trim()))
// 否则使用站点
.orElseGet(this::getEndpointUrl);
}
@@ -274,7 +273,8 @@ public class OssClientConfig implements Config<OssClientConfig, OssClientConfig.
*/
private String getAccessBaseUrl() {
return domain()
.filter(OssClientConfig::hasHttpHeader)
.filter(StringUtils::isNotBlank)
.map(String::trim)
.orElseGet(this::getEndpoint);
}
@@ -289,16 +289,6 @@ public class OssClientConfig implements Config<OssClientConfig, OssClientConfig.
.orElseThrow(() -> S3StorageException.form("endpoint is not configured."));
}
/**
* 判断 URL 是否包含 HTTP 协议头。
*
* @param url URL
* @return 是否包含 HTTP 协议头
*/
private static boolean hasHttpHeader(String url) {
return HttpUtil.isHttp(url) || HttpUtil.isHttps(url);
}
/**
* ACL访问策略配置
*/
@@ -31,4 +31,9 @@ public interface MessageConstants {
* 心跳响应标识
*/
String PONG = "pong";
/**
* 同一 token 的新连接替换旧连接时发送给旧连接的控制消息。
*/
String KICKED = "kicked";
}
@@ -65,6 +65,7 @@ public class SseEmitterSessionManager implements PushSessionManager {
// 关闭已存在的SseEmitter,防止超过最大连接数
SseEmitter oldEmitter = emitters.remove(token);
if (oldEmitter != null) {
sendKickedMessage(oldEmitter);
oldEmitter.complete();
}
@@ -103,6 +104,21 @@ public class SseEmitterSessionManager implements PushSessionManager {
return emitter;
}
/**
* 通知旧连接已被同 token 新连接替换。
*
* @param emitter 旧 SSE 连接
*/
private void sendKickedMessage(SseEmitter emitter) {
try {
emitter.send(SseEmitter.event()
.name("message")
.data(MessageConstants.KICKED));
} catch (Exception ignore) {
// 旧连接可能已断开,忽略通知失败
}
}
/**
* 断开指定用户的 SSE 连接
*
@@ -5,6 +5,7 @@ import cn.hutool.core.map.MapUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.ThreadUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.push.constant.MessageConstants;
import org.dromara.common.push.dto.PushDTO;
import org.dromara.common.push.properties.MessageProperties;
import org.dromara.common.redis.utils.RedisUtils;
@@ -61,6 +62,7 @@ public class WebSocketSessionManager implements PushSessionManager {
Map<String, WebSocketSession> sessions = USER_TOKEN_SESSIONS.computeIfAbsent(userId, key -> new ConcurrentHashMap<>());
// 移除并关闭旧的同token会话,避免重复连接
WebSocketSession oldSession = sessions.remove(token);
sendKickedMessage(oldSession);
closeSession(oldSession, CloseStatus.NORMAL);
// 存储新会话
sessions.put(token, session);
@@ -129,6 +131,18 @@ public class WebSocketSessionManager implements PushSessionManager {
toRemoveUsers.forEach(USER_TOKEN_SESSIONS::remove);
}
/**
* 通知旧连接已被同 token 新连接替换。
*
* @param session 旧 WebSocket 会话
*/
private void sendKickedMessage(WebSocketSession session) {
if (session == null || !session.isOpen()) {
return;
}
sendMessage(session, MessageConstants.KICKED);
}
/**
* 订阅消息通道
* 注册消息消费者,监听Redis消息推送
@@ -38,8 +38,8 @@ public class CacheConfig {
* 自定义缓存管理器 整合spring-cache
*/
@Bean
public CacheManager cacheManager() {
return new PlusSpringCacheManager();
public CacheManager cacheManager(Cache<Object, Object> caffeine) {
return new PlusSpringCacheManager(caffeine);
}
}
@@ -0,0 +1,32 @@
package org.dromara.common.redis.config;
import cn.hutool.http.HttpStatus;
import com.baomidou.lock.LockFailureStrategy;
import com.baomidou.lock.spring.boot.autoconfigure.LockAutoConfiguration;
import org.dromara.common.core.exception.ServiceException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Role;
/**
* Lock4j 配置
*
* @author AprilWind
*/
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@AutoConfiguration(before = LockAutoConfiguration.class)
public class Lock4jConfig {
/**
* Lock4j 获取锁失败策略。
*/
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public LockFailureStrategy defaultLockFailureStrategy() {
return (key, method, arguments) -> {
throw new ServiceException("业务处理中,请稍后再试...", HttpStatus.HTTP_UNAVAILABLE);
};
}
}
@@ -2,3 +2,4 @@ org.dromara.common.redis.config.RedisConfig
org.dromara.common.redis.config.CacheConfig
org.dromara.common.redis.config.IdempotentConfig
org.dromara.common.redis.config.RateLimiterConfig
org.dromara.common.redis.config.Lock4jConfig
@@ -11,6 +11,10 @@
<artifactId>ruoyi-common-satoken</artifactId>
<description>
ruoyi-common-satoken 权限认证
</description>
<dependencies>
<!-- 核心模块 -->
@@ -46,10 +46,10 @@ public class SaTokenExceptionHandler {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, e.getMessage());
String msg = switch (e.getType()) {
case NotLoginException.TOKEN_TIMEOUT -> "登录已过期,请重新登录";
case NotLoginException.TOKEN_TIMEOUT,
NotLoginException.TOKEN_FREEZE -> "登录已过期,请重新登录";
case NotLoginException.BE_REPLACED -> "当前账号已在其他设备登录,您已被强制下线";
case NotLoginException.KICK_OUT -> "账号已被管理员强制下线";
case NotLoginException.TOKEN_FREEZE -> "账号已被冻结,请联系管理员处理";
default -> "登录状态异常,请重新登录";
};
return R.fail(HttpStatus.HTTP_UNAUTHORIZED, msg);
+1 -7
View File
@@ -18,16 +18,10 @@
<dependencies>
<!-- 授权认证 -->
<dependency>
<groupId>me.zhyd.oauth</groupId>
<groupId>io.github.windtool</groupId>
<artifactId>JustAuth</artifactId>
</dependency>
<!-- 序列化模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-json</artifactId>
</dependency>
<!-- 缓存服务 -->
<dependency>
<groupId>org.dromara</groupId>
@@ -1,179 +0,0 @@
package me.zhyd.oauth.request;
import com.alibaba.fastjson.JSONObject;
import me.zhyd.oauth.cache.AuthStateCache;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.config.AuthSource;
import me.zhyd.oauth.enums.AuthResponseStatus;
import me.zhyd.oauth.enums.AuthUserGender;
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.HttpUtils;
import me.zhyd.oauth.utils.StringUtils;
import me.zhyd.oauth.utils.UrlBuilder;
/**
* <p>
* 企业微信登录父类
* </p>
*
* @author liguanhua (347826496(a)qq.com)
* @since 1.15.9
*/
public abstract class AbstractAuthWeChatEnterpriseRequest extends AuthDefaultRequest {
/**
* 创建企业微信认证请求。
*
* @param config 授权配置
* @param source 授权来源
*/
public AbstractAuthWeChatEnterpriseRequest(AuthConfig config, AuthSource source) {
super(config, source);
}
/**
* 创建企业微信认证请求。
*
* @param config 授权配置
* @param source 授权来源
* @param authStateCache 授权状态缓存
*/
public AbstractAuthWeChatEnterpriseRequest(AuthConfig config, AuthSource source, AuthStateCache authStateCache) {
super(config, source, authStateCache);
}
/**
* 获取企业微信访问令牌。
*
* @param authCallback 授权回调参数
* @return 访问令牌
*/
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
String response = doGetAuthorizationCode(accessTokenUrl(null));
JSONObject object = this.checkResponse(response);
return AuthToken.builder()
.accessToken(object.getString("access_token"))
.expireIn(object.getIntValue("expires_in"))
.code(authCallback.getCode())
.build();
}
/**
* 获取企业微信用户信息。
*
* @param authToken 访问令牌
* @return 授权用户信息
*/
@Override
public AuthUser getUserInfo(AuthToken authToken) {
String response = doGetUserInfo(authToken);
JSONObject object = this.checkResponse(response);
// 返回 OpenId 或其他,均代表非当前企业用户,不支持
// https://github.com/justauth/JustAuth/issues/227 修复bug
if (!object.containsKey("userid")) {
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"))
.nickname(userDetail.getString("alias"))
.avatar(userDetail.getString("avatar"))
.location(userDetail.getString("address"))
.email(userDetail.getString("email"))
.uuid(userId)
.gender(AuthUserGender.getWechatRealGender(userDetail.getString("gender")))
.token(authToken)
.source(source.toString())
.build();
}
/**
* 校验请求结果
*
* @param response 请求结果
* @return 如果请求结果正常,则返回JSONObject
*/
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);
}
return object;
}
/**
* 返回获取accessToken的url
*
* @param code 授权码
* @return 返回获取accessToken的url
*/
@Override
protected String accessTokenUrl(String code) {
return UrlBuilder.fromBaseUrl(source.accessToken())
.queryParam("corpid", config.getClientId())
.queryParam("corpsecret", config.getClientSecret())
.build();
}
/**
* 返回获取userInfo的url
*
* @param authToken 用户授权后的token
* @return 返回获取userInfo的url
*/
@Override
protected String userInfoUrl(AuthToken authToken) {
return UrlBuilder.fromBaseUrl(source.userInfo())
.queryParam("access_token", authToken.getAccessToken())
.queryParam("code", authToken.getCode())
.build();
}
/**
* 用户详情
*
* @param accessToken accessToken
* @param userId 企业内用户id
* @param userTicket 成员票据,用于获取用户信息或敏感信息
* @return 用户详情
*/
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)
.build();
String userInfoResponse = new HttpUtils(config.getHttpConfig()).get(userInfoUrl).getBody();
JSONObject userInfo = checkResponse(userInfoResponse);
// 用户敏感信息
if (StringUtils.isNotEmpty(userTicket)) {
String userDetailUrl = UrlBuilder.fromBaseUrl("https://qyapi.weixin.qq.com/cgi-bin/auth/getuserdetail")
.queryParam("access_token", accessToken)
.build();
JSONObject param = new JSONObject();
param.put("user_ticket", userTicket);
String userDetailResponse = new HttpUtils(config.getHttpConfig()).post(userDetailUrl, param.toJSONString()).getBody();
JSONObject userDetail = checkResponse(userDetailResponse);
userInfo.putAll(userDetail);
}
return userInfo;
}
}
@@ -1,138 +0,0 @@
package me.zhyd.oauth.request;
import com.alibaba.fastjson.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.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;
import java.util.HashMap;
import java.util.Map;
/**
* 新版钉钉二维码登录
*
* @author yadong.zhang (yadong.zhang0415(a)gmail.com)
* @since 1.16.7
*/
public class AuthDingTalkV2Request extends AuthDefaultRequest {
/**
* 创建钉钉 V2 认证请求。
*
* @param config 授权配置
*/
public AuthDingTalkV2Request(AuthConfig config) {
super(config, AuthDefaultSource.DINGTALK_V2);
}
/**
* 创建钉钉 V2 认证请求。
*
* @param config 授权配置
* @param authStateCache 授权状态缓存
*/
public AuthDingTalkV2Request(AuthConfig config, AuthStateCache authStateCache) {
super(config, AuthDefaultSource.DINGTALK_V2, authStateCache);
}
/**
* 生成钉钉 V2 授权地址。
*
* @param state 授权状态
* @return 授权地址
*/
@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("prompt", "consent")
.queryParam("org_type", config.getDingTalkOrgType())
.queryParam("corpId", config.getDingTalkCorpId())
.queryParam("exclusiveLogin", config.isDingTalkExclusiveLogin())
.queryParam("exclusiveCorpId", config.getDingTalkExclusiveCorpId())
.queryParam("state", getRealState(state))
.build();
}
/**
* 获取钉钉 V2 访问令牌。
*
* @param authCallback 授权回调参数
* @return 访问令牌
*/
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
Map<String, String> params = new HashMap<>();
params.put("grantType", "authorization_code");
params.put("clientId", config.getClientId());
params.put("clientSecret", config.getClientSecret());
params.put("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")) {
throw new AuthException(JSONObject.toJSONString(response), source);
}
return AuthToken.builder()
.accessToken(accessTokenObject.getString("accessToken"))
.refreshToken(accessTokenObject.getString("refreshToken"))
.expireIn(accessTokenObject.getIntValue("expireIn"))
.corpId(accessTokenObject.getString("corpId"))
.build();
}
/**
* 获取钉钉 V2 用户信息。
*
* @param authToken 访问令牌
* @return 授权用户信息
*/
@Override
public AuthUser getUserInfo(AuthToken authToken) {
HttpHeader header = new HttpHeader();
header.add("x-acs-dingtalk-access-token", authToken.getAccessToken());
String response = new HttpUtils(config.getHttpConfig()).get(this.source.userInfo(), null, header, false).getBody();
JSONObject object = JSONObject.parseObject(response);
authToken.setOpenId(object.getString("openId"));
authToken.setUnionId(object.getString("unionId"));
return AuthUser.builder()
.rawUserInfo(object)
.uuid(object.getString("unionId"))
.username(object.getString("nick"))
.nickname(object.getString("nick"))
.avatar(object.getString("avatarUrl"))
.snapshotUser(object.getBooleanValue("visitor"))
.token(authToken)
.source(source.toString())
.build();
}
/**
* 返回获取accessToken的url
*
* @param code 授权码
* @return 返回获取accessToken的url
*/
protected String accessTokenUrl(String code) {
return UrlBuilder.fromBaseUrl(source.accessToken())
.queryParam("code", code)
.queryParam("clientId", config.getClientId())
.queryParam("clientSecret", config.getClientSecret())
.queryParam("grantType", "authorization_code")
.build();
}
}
@@ -68,7 +68,7 @@ public class SocialLoginConfigProperties {
private String clientOsType;
/**
* maxkey 服务器地址
* 自托管授权服务器地址
*/
private String serverUrl;
@@ -1,119 +0,0 @@
package org.dromara.common.social.gitea;
import cn.hutool.core.lang.Dict;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.cache.AuthStateCache;
import me.zhyd.oauth.config.AuthConfig;
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.request.AuthDefaultRequest;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.json.utils.JsonUtils;
/**
* @author lcry
*/
@Slf4j
public class AuthGiteaRequest extends AuthDefaultRequest {
/**
* 服务器地址。
*/
public static final String SERVER_URL = SpringUtils.getProperty("justauth.type.gitea.server-url");
/**
* 设定归属域
*/
public AuthGiteaRequest(AuthConfig config) {
super(config, AuthGiteaSource.GITEA);
}
/**
* 创建 Gitea 认证请求。
*
* @param config 授权配置
* @param authStateCache 授权状态缓存
*/
public AuthGiteaRequest(AuthConfig config, AuthStateCache authStateCache) {
super(config, AuthGiteaSource.GITEA, authStateCache);
}
/**
* 获取 Gitea 访问令牌。
*
* @param authCallback 授权回调参数
* @return 访问令牌
*/
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
String body = doPostAuthorizationCode(authCallback.getCode());
Dict object = JsonUtils.parseMap(body);
// oauth/token 验证异常
if (object.containsKey("error")) {
throw new AuthException(object.getStr("error_description"));
}
// user 验证异常
if (object.containsKey("message")) {
throw new AuthException(object.getStr("message"));
}
return AuthToken.builder()
.accessToken(object.getStr("access_token"))
.refreshToken(object.getStr("refresh_token"))
.idToken(object.getStr("id_token"))
.tokenType(object.getStr("token_type"))
.scope(object.getStr("scope"))
.build();
}
/**
* 使用授权码换取 Gitea 访问令牌响应。
*
* @param code 授权码
* @return 令牌接口响应体
*/
@Override
protected String doPostAuthorizationCode(String code) {
HttpRequest request = HttpRequest.post(source.accessToken())
.form("client_id", config.getClientId())
.form("client_secret", config.getClientSecret())
.form("grant_type", "authorization_code")
.form("code", code)
.form("redirect_uri", config.getRedirectUri());
HttpResponse response = request.execute();
return response.body();
}
/**
* 获取 Gitea 用户信息。
*
* @param authToken 访问令牌
* @return 授权用户信息
*/
@Override
public AuthUser getUserInfo(AuthToken authToken) {
String body = doGetUserInfo(authToken);
Dict object = JsonUtils.parseMap(body);
// oauth/token 验证异常
if (object.containsKey("error")) {
throw new AuthException(object.getStr("error_description"));
}
// user 验证异常
if (object.containsKey("message")) {
throw new AuthException(object.getStr("message"));
}
return AuthUser.builder()
.uuid(object.getStr("sub"))
.username(object.getStr("name"))
.nickname(object.getStr("preferred_username"))
.avatar(object.getStr("picture"))
.email(object.getStr("email"))
.token(authToken)
.source(source.toString())
.build();
}
}
@@ -1,50 +0,0 @@
package org.dromara.common.social.gitea;
import me.zhyd.oauth.config.AuthSource;
import me.zhyd.oauth.request.AuthDefaultRequest;
/**
* gitea Oauth2 默认接口说明
*
* @author lcry
*/
public enum AuthGiteaSource implements AuthSource {
/**
* 自己搭建的 gitea 私服
*/
GITEA {
/**
* 授权的api
*/
@Override
public String authorize() {
return AuthGiteaRequest.SERVER_URL + "/login/oauth/authorize";
}
/**
* 获取accessToken的api
*/
@Override
public String accessToken() {
return AuthGiteaRequest.SERVER_URL + "/login/oauth/access_token";
}
/**
* 获取用户信息的api
*/
@Override
public String userInfo() {
return AuthGiteaRequest.SERVER_URL + "/login/oauth/userinfo";
}
/**
* 平台对应的 AuthRequest 实现类,必须继承自 {@link AuthDefaultRequest}
*/
@Override
public Class<? extends AuthDefaultRequest> getTargetClass() {
return AuthGiteaRequest.class;
}
}
}
@@ -1,101 +0,0 @@
package org.dromara.common.social.maxkey;
import cn.hutool.core.lang.Dict;
import me.zhyd.oauth.cache.AuthStateCache;
import me.zhyd.oauth.config.AuthConfig;
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.request.AuthDefaultRequest;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.json.utils.JsonUtils;
/**
* @author 长春叭哥 2023年03月26日
*/
public class AuthMaxKeyRequest extends AuthDefaultRequest {
/**
* 服务器地址。
*/
public static final String SERVER_URL = SpringUtils.getProperty("justauth.type.maxkey.server-url");
/**
* 设定归属域
*/
public AuthMaxKeyRequest(AuthConfig config) {
super(config, AuthMaxKeySource.MAXKEY);
}
/**
* 创建 MaxKey 认证请求。
*
* @param config 授权配置
* @param authStateCache 授权状态缓存
*/
public AuthMaxKeyRequest(AuthConfig config, AuthStateCache authStateCache) {
super(config, AuthMaxKeySource.MAXKEY, authStateCache);
}
/**
* 获取 MaxKey 访问令牌。
*
* @param authCallback 授权回调参数
* @return 访问令牌
*/
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
String body = doPostAuthorizationCode(authCallback.getCode());
Dict object = JsonUtils.parseMap(body);
// oauth/token 验证异常
if (object.containsKey("error")) {
throw new AuthException(object.getStr("error_description"));
}
// user 验证异常
if (object.containsKey("message")) {
throw new AuthException(object.getStr("message"));
}
return AuthToken.builder()
.accessToken(object.getStr("access_token"))
.refreshToken(object.getStr("refresh_token"))
.idToken(object.getStr("id_token"))
.tokenType(object.getStr("token_type"))
.scope(object.getStr("scope"))
.build();
}
/**
* 获取 MaxKey 用户信息。
*
* @param authToken 访问令牌
* @return 授权用户信息
*/
@Override
public AuthUser getUserInfo(AuthToken authToken) {
String body = doGetUserInfo(authToken);
Dict object = JsonUtils.parseMap(body);
// oauth/token 验证异常
if (object.containsKey("error")) {
throw new AuthException(object.getStr("error_description"));
}
// user 验证异常
if (object.containsKey("message")) {
throw new AuthException(object.getStr("message"));
}
return AuthUser.builder()
.uuid(object.getStr("userId"))
.username(object.getStr("username"))
.nickname(object.getStr("displayName"))
.avatar(object.getStr("avatar_url"))
.blog(object.getStr("web_url"))
.company(object.getStr("organization"))
.location(object.getStr("location"))
.email(object.getStr("email"))
.remark(object.getStr("bio"))
.token(authToken)
.source(source.toString())
.build();
}
}
@@ -1,52 +0,0 @@
package org.dromara.common.social.maxkey;
import me.zhyd.oauth.config.AuthSource;
import me.zhyd.oauth.request.AuthDefaultRequest;
/**
* Oauth2 默认接口说明
*
* @author 长春叭哥 2023年03月26日
*
*/
public enum AuthMaxKeySource implements AuthSource {
/**
* 自己搭建的 maxkey 私服
*/
MAXKEY {
/**
* 授权的api
*/
@Override
public String authorize() {
return AuthMaxKeyRequest.SERVER_URL + "/sign/authz/oauth/v20/authorize";
}
/**
* 获取accessToken的api
*/
@Override
public String accessToken() {
return AuthMaxKeyRequest.SERVER_URL + "/sign/authz/oauth/v20/token";
}
/**
* 获取用户信息的api
*/
@Override
public String userInfo() {
return AuthMaxKeyRequest.SERVER_URL + "/sign/api/oauth/v20/me";
}
/**
* 平台对应的 AuthRequest 实现类,必须继承自 {@link AuthDefaultRequest}
*/
@Override
public Class<? extends AuthDefaultRequest> getTargetClass() {
return AuthMaxKeyRequest.class;
}
}
}
@@ -1,156 +0,0 @@
package org.dromara.common.social.topiam;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.xkcoding.http.support.HttpHeader;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.cache.AuthStateCache;
import me.zhyd.oauth.config.AuthConfig;
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.request.AuthDefaultRequest;
import me.zhyd.oauth.utils.HttpUtils;
import me.zhyd.oauth.utils.UrlBuilder;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.json.utils.JsonUtils;
import static org.dromara.common.social.topiam.AuthTopIamSource.TOPIAM;
/**
* TopIAM 认证请求
*
* @author xlsea
* @since 2024-01-06
*/
@Slf4j
public class AuthTopIamRequest extends AuthDefaultRequest {
/**
* 服务器地址。
*/
public static final String SERVER_URL = SpringUtils.getProperty("justauth.type.topiam.server-url");
/**
* 设定归属域
*/
public AuthTopIamRequest(AuthConfig config) {
super(config, TOPIAM);
}
/**
* 创建 TopIAM 认证请求。
*
* @param config 授权配置
* @param authStateCache 授权状态缓存
*/
public AuthTopIamRequest(AuthConfig config, AuthStateCache authStateCache) {
super(config, TOPIAM, authStateCache);
}
/**
* 获取 TopIAM 访问令牌。
*
* @param authCallback 授权回调参数
* @return 访问令牌
*/
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
String body = doPostAuthorizationCode(authCallback.getCode());
Dict object = JsonUtils.parseMap(body);
checkResponse(object);
return AuthToken.builder()
.accessToken(object.getStr("access_token"))
.refreshToken(object.getStr("refresh_token"))
.idToken(object.getStr("id_token"))
.tokenType(object.getStr("token_type"))
.scope(object.getStr("scope"))
.build();
}
/**
* 获取 TopIAM 用户信息。
*
* @param authToken 访问令牌
* @return 授权用户信息
*/
@Override
public AuthUser getUserInfo(AuthToken authToken) {
String body = doGetUserInfo(authToken);
Dict object = JsonUtils.parseMap(body);
checkResponse(object);
return AuthUser.builder()
.uuid(object.getStr("sub"))
.username(object.getStr("preferred_username"))
.nickname(object.getStr("nickname"))
.avatar(object.getStr("picture"))
.email(object.getStr("email"))
.token(authToken)
.source(source.toString())
.build();
}
/**
* 使用授权码换取 TopIAM 访问令牌响应。
*
* @param code 授权码
* @return 令牌接口响应体
*/
@Override
protected String doPostAuthorizationCode(String code) {
HttpRequest request = HttpRequest.post(source.accessToken())
.header("Authorization", "Basic " + Base64.encode("%s:%s".formatted(config.getClientId(), config.getClientSecret())))
.form("grant_type", "authorization_code")
.form("code", code)
.form("redirect_uri", config.getRedirectUri());
HttpResponse response = request.execute();
return response.body();
}
/**
* 获取 TopIAM 用户信息接口响应。
*
* @param authToken 访问令牌
* @return 用户信息响应体
*/
@Override
protected String doGetUserInfo(AuthToken authToken) {
return new HttpUtils(config.getHttpConfig()).get(source.userInfo(), null, new HttpHeader()
.add("Content-Type", "application/json")
.add("Authorization", "Bearer " + authToken.getAccessToken()), false).getBody();
}
/**
* 生成 TopIAM 授权地址。
*
* @param state 授权状态
* @return 授权地址
*/
@Override
public String authorize(String state) {
return UrlBuilder.fromBaseUrl(super.authorize(state))
.queryParam("scope", StrUtil.join("%20", config.getScopes()))
.build();
}
/**
* 校验 TopIAM 接口响应。
*
* @param object 响应内容
*/
private static void checkResponse(Dict object) {
// oauth/token 验证异常
if (object.containsKey("error")) {
throw new AuthException(object.getStr("error_description"));
}
// user 验证异常
if (object.containsKey("message")) {
throw new AuthException(object.getStr("message"));
}
}
}
@@ -1,51 +0,0 @@
package org.dromara.common.social.topiam;
import me.zhyd.oauth.config.AuthSource;
import me.zhyd.oauth.request.AuthDefaultRequest;
/**
* Oauth2 默认接口说明
*
* @author xlsea
* @since 2024-01-06
*/
public enum AuthTopIamSource implements AuthSource {
/**
* 测试
*/
TOPIAM {
/**
* 授权的api
*/
@Override
public String authorize() {
return AuthTopIamRequest.SERVER_URL + "/oauth2/auth";
}
/**
* 获取accessToken的api
*/
@Override
public String accessToken() {
return AuthTopIamRequest.SERVER_URL + "/oauth2/token";
}
/**
* 获取用户信息的api
*/
@Override
public String userInfo() {
return AuthTopIamRequest.SERVER_URL + "/oauth2/userinfo";
}
/**
* 平台对应的 AuthRequest 实现类,必须继承自 {@link AuthDefaultRequest}
*/
@Override
public Class<? extends AuthDefaultRequest> getTargetClass() {
return AuthTopIamRequest.class;
}
}
}
@@ -10,9 +10,6 @@ import me.zhyd.oauth.request.*;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
import org.dromara.common.social.config.properties.SocialProperties;
import org.dromara.common.social.gitea.AuthGiteaRequest;
import org.dromara.common.social.maxkey.AuthMaxKeyRequest;
import org.dromara.common.social.topiam.AuthTopIamRequest;
/**
* 认证授权工具类
@@ -58,6 +55,7 @@ public class SocialUtils {
.clientId(obj.getClientId())
.clientSecret(obj.getClientSecret())
.redirectUri(obj.getRedirectUri())
.serverUrl(obj.getServerUrl())
.scopes(obj.getScopes());
return switch (source.toLowerCase()) {
case "dingtalk" -> new AuthDingTalkV2Request(builder.build(), STATE_CACHE);
+2 -2
View File
@@ -1,6 +1,6 @@
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
FROM bellsoft/liberica-openjdk-rocky:21.0.11-cds
# FROM bellsoft/liberica-openjdk-rocky:25.0.3-cds
FROM bellsoft/liberica-openjdk-rocky:21.0.12-cds
# FROM bellsoft/liberica-openjdk-rocky:25.0.4-cds
# FROM findepi/graalvm:java21-native
LABEL maintainer="Lion Li"
+2 -2
View File
@@ -1,6 +1,6 @@
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
FROM bellsoft/liberica-openjdk-rocky:21.0.11-cds
# FROM bellsoft/liberica-openjdk-rocky:25.0.3-cds
FROM bellsoft/liberica-openjdk-rocky:21.0.12-cds
# FROM bellsoft/liberica-openjdk-rocky:25.0.4-cds
# FROM findepi/graalvm:java21-native
LABEL maintainer="Lion Li"
+11
View File
@@ -19,6 +19,17 @@
<version>${snailai.version}</version>
</dependency>
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-rest5-client</artifactId>
<version>9.4.2</version>
</dependency>
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>9.4.2</version>
</dependency>
<!-- Spring Boot Admin 客户端 -->
<dependency>
<groupId>de.codecentric</groupId>
@@ -17,7 +17,7 @@ public class SnailAiServerApplication {
* @param args 启动参数
*/
public static void main(String[] args) {
com.aizuda.snail.ai.starter.SnailAiSpringbootApplication.main(args);
com.aizuda.snail.ai.starter.SnailAiApplication.main(args);
}
}
@@ -35,6 +35,24 @@ snail-ai:
access-key: minioadmin
secret-key: minioadmin
bucket: snail-ai
# RAG 文档解析与图片 OCR 配置
rag:
docling:
enabled: ${SNAIL_AI_RAG_DOCLING_ENABLED:true}
url: ${SNAIL_AI_RAG_DOCLING_URL:http://127.0.0.1:5100}
timeout-seconds: ${SNAIL_AI_RAG_DOCLING_TIMEOUT_SECONDS:300}
concurrency: ${SNAIL_AI_RAG_DOCLING_CONCURRENCY:3}
health-timeout-millis: ${SNAIL_AI_RAG_DOCLING_HEALTH_TIMEOUT_MILLIS:3000}
poll-interval-millis: ${SNAIL_AI_RAG_DOCLING_POLL_INTERVAL_MILLIS:3000}
status-timeout-millis: ${SNAIL_AI_RAG_DOCLING_STATUS_TIMEOUT_MILLIS:10000}
result-timeout-millis: ${SNAIL_AI_RAG_DOCLING_RESULT_TIMEOUT_MILLIS:30000}
max-image-count: ${SNAIL_AI_RAG_DOCLING_MAX_IMAGE_COUNT:100}
max-image-bytes: ${SNAIL_AI_RAG_DOCLING_MAX_IMAGE_BYTES:10485760}
paddle-ocr-enabled: ${SNAIL_AI_RAG_DOCLING_PADDLE_OCR_ENABLED:true}
paddle-ocr-url: ${SNAIL_AI_RAG_DOCLING_PADDLE_OCR_URL:http://127.0.0.1:8866/ocr}
paddle-ocr-timeout-millis: ${SNAIL_AI_RAG_DOCLING_PADDLE_OCR_TIMEOUT_MILLIS:180000}
paddle-ocr-batch-size: ${SNAIL_AI_RAG_DOCLING_PADDLE_OCR_BATCH_SIZE:5}
vision-ocr-fallback-enabled: ${SNAIL_AI_RAG_DOCLING_VISION_OCR_FALLBACK_ENABLED:false}
# 短期记忆配置
memory:
short-term:
@@ -1,6 +1,6 @@
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
FROM bellsoft/liberica-openjdk-rocky:21.0.11-cds
# FROM bellsoft/liberica-openjdk-rocky:25.0.3-cds
FROM bellsoft/liberica-openjdk-rocky:21.0.12-cds
# FROM bellsoft/liberica-openjdk-rocky:25.0.4-cds
# FROM findepi/graalvm:java21-native
LABEL maintainer="Lion Li"
@@ -11,6 +11,7 @@ import org.dromara.common.core.domain.R;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.web.core.BaseController;
import org.dromara.system.api.model.LoginUser;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -27,6 +28,7 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/snail-ai")
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "snail-ai.open-api", name = "enabled", havingValue = "true")
public class SnailAiController extends BaseController {
/**
@@ -1,14 +1,11 @@
package org.dromara.gen.config.properties;
import cn.hutool.extra.template.TemplateConfig;
import lombok.Data;
import org.dromara.common.core.factory.YmlPropertySourceFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
/**
* 代码生成配置属性
*
@@ -40,9 +37,4 @@ public class GenProperties {
*/
private String tablePrefix;
/**
* 模板配置
*/
private TemplateConfig templateConfig = new TemplateConfig(StandardCharsets.UTF_8, null, TemplateConfig.ResourceMode.CLASSPATH);
}
@@ -85,6 +85,11 @@ public interface GenConstants {
*/
String FRONTEND_TYPE_VUE = "vue";
/**
* React 前端模板类型,对应模板根目录下的 react 目录。
*/
String FRONTEND_TYPE_REACT = "react";
/**
* 树根节点值
*/
@@ -103,38 +108,39 @@ public interface GenConstants {
/**
* 数据库字符串类型
*/
String[] COLUMNTYPE_STR = {"char", "varchar", "enum", "set", "nchar", "nvarchar", "varchar2", "nvarchar2"};
String[] COLUMNTYPE_STR = {"char", "varchar", "enum", "set", "nchar", "nvarchar", "varchar2", "nvarchar2",
"long varchar", "long nvarchar", "uniqueidentifier", "uuid", "json", "jsonb", "xml"};
/**
* 数据库文本类型
*/
String[] COLUMNTYPE_TEXT = {"tinytext", "text", "mediumtext", "longtext", "binary", "varbinary", "blob",
"ntext", "image", "bytea"};
"ntext", "image", "bytea", "clob", "nclob", "long", "long raw", "raw"};
/**
* 数据库时间类型
*/
String[] COLUMNTYPE_TIME = {"datetime", "time", "date", "timestamp", "year", "interval",
"smalldatetime", "datetime2", "datetimeoffset", "timestamptz"};
"smalldatetime", "datetime2", "datetimeoffset", "timestamptz", "timestamp with time zone",
"timestamp without time zone", "time with time zone", "time without time zone"};
/**
* 数据库数字类型
*/
String[] COLUMNTYPE_NUMBER = {"tinyint", "smallint", "mediumint", "int", "int2", "int4", "int8", "number", "integer",
"bit", "bigint", "float", "float4", "float8", "double", "decimal", "numeric", "real", "double precision",
"smallserial", "serial", "bigserial", "money", "smallmoney"};
"bit", "boolean", "bool", "bigint", "float", "float4", "float8", "double", "decimal", "dec", "fixed",
"numeric", "real", "double precision", "binary_float", "binary_double", "smallserial", "serial",
"serial2", "serial4", "serial8", "bigserial", "money", "smallmoney"};
/**
* BO对象 不需要添加字段
*/
String[] COLUMNNAME_NOT_ADD = {"create_dept", "create_by", "create_time", "del_flag", "update_by",
"update_time", "version"};
String[] COLUMNNAME_NOT_ADD = {"del_flag", "version"};
/**
* BO对象 不需要编辑字段
*/
String[] COLUMNNAME_NOT_EDIT = {"create_dept", "create_by", "create_time", "del_flag", "update_by",
"update_time", "version"};
String[] COLUMNNAME_NOT_EDIT = {"del_flag", "version"};
/**
* VO对象 不需要返回字段
@@ -145,8 +151,7 @@ public interface GenConstants {
/**
* BO对象 不需要查询字段
*/
String[] COLUMNNAME_NOT_QUERY = {"id", "create_dept", "create_by", "create_time", "del_flag", "update_by",
"update_time", "remark", "version"};
String[] COLUMNNAME_NOT_QUERY = {"id", "del_flag", "remark", "version"};
/**
* Entity基类字段
@@ -228,6 +233,11 @@ public interface GenConstants {
*/
String TYPE_DOUBLE = "Double";
/**
* 单精度浮点型
*/
String TYPE_FLOAT = "Float";
/**
* 高精度计算类型
*/
@@ -156,7 +156,7 @@ public class GenTableColumn extends BaseEntity {
}
if (StringUtils.equalsAny(this.javaType,
GenConstants.TYPE_LONG, GenConstants.TYPE_INTEGER, GenConstants.TYPE_DOUBLE,
"Float", GenConstants.TYPE_BIGDECIMAL)) {
GenConstants.TYPE_FLOAT, GenConstants.TYPE_BIGDECIMAL)) {
return "number";
}
if (StringUtils.equals(this.javaType, GenConstants.TYPE_BOOLEAN)) {
@@ -174,7 +174,7 @@ public class GenTableColumn extends BaseEntity {
if (StringUtils.equals(this.javaType, GenConstants.TYPE_BOOLEAN)) {
return "true";
}
if (StringUtils.equalsAny(this.javaType, GenConstants.TYPE_INTEGER, GenConstants.TYPE_LONG)) {
if (isNumberType()) {
return "0";
}
return "'0'";
@@ -189,12 +189,23 @@ public class GenTableColumn extends BaseEntity {
if (StringUtils.equals(this.javaType, GenConstants.TYPE_BOOLEAN)) {
return "false";
}
if (StringUtils.equalsAny(this.javaType, GenConstants.TYPE_INTEGER, GenConstants.TYPE_LONG)) {
if (isNumberType()) {
return "1";
}
return "'1'";
}
/**
* 判断当前字段是否为数值类型。
*
* @return 数值类型返回 {@code true}
*/
public boolean isNumberType() {
return StringUtils.equalsAny(this.javaType,
GenConstants.TYPE_INTEGER, GenConstants.TYPE_LONG, GenConstants.TYPE_DOUBLE,
GenConstants.TYPE_FLOAT, GenConstants.TYPE_BIGDECIMAL);
}
/**
* 是否为日期范围查询字段。
*
@@ -55,7 +55,7 @@ public class GenTableServiceImpl implements IGenTableService {
private final GenTableMapper tableMapper;
private final GenTableColumnMapper genTableColumnMapper;
private static final String[] TABLE_IGNORE = new String[]{"sj_", "flow_", "gen_"};
private static final String[] TABLE_IGNORE = new String[]{"sai_", "sj_", "flow_", "gen_"};
/**
* 查询业务字段列表
@@ -46,7 +46,7 @@ public class GenUtils {
* @param table 所属业务表对象
*/
public static void initColumnField(GenTableColumn column, GenTable table) {
String dataType = getDbType(column.getColumnType()).toLowerCase();
String dataType = normalizeDbType(getDbType(column.getColumnType()));
// 统一转小写 避免有些数据库默认大写问题 如果需要特别书写方式 请在实体类增加注解标注别名
String columnName = column.getColumnName().toLowerCase();
Integer columnLength = getColumnLength(column.getColumnType());
@@ -58,6 +58,8 @@ public class GenUtils {
column.setJavaField(StringUtils.toCamelCase(columnName));
// 设置默认类型
column.setJavaType(GenConstants.TYPE_STRING);
// 设置默认HTML类型
column.setHtmlType(GenConstants.HTML_INPUT);
column.setQueryType(GenConstants.QUERY_EQ);
if (arraysContains(GenConstants.COLUMNTYPE_STR, dataType) || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType)) {
@@ -148,16 +150,20 @@ public class GenUtils {
if (isBooleanColumn(dataType, columnLength, columnScale, columnName)) {
return GenConstants.TYPE_BOOLEAN;
}
if (arraysContains(new String[]{"decimal", "numeric", "money", "smallmoney"}, dataType)) {
return columnScale > 0 ? GenConstants.TYPE_BIGDECIMAL : resolveIntegerJavaType(columnLength);
if (arraysContains(new String[]{"decimal", "dec", "fixed", "numeric", "money", "smallmoney"}, dataType)) {
return GenConstants.TYPE_BIGDECIMAL;
}
if (arraysContains(new String[]{"float", "float4", "float8", "double", "real", "double precision"}, dataType)) {
if (arraysContains(new String[]{"float4", "real", "binary_float"}, dataType)) {
return GenConstants.TYPE_FLOAT;
}
if (arraysContains(new String[]{"float", "float8", "double", "double precision", "binary_double"}, dataType)) {
return GenConstants.TYPE_DOUBLE;
}
if (arraysContains(new String[]{"bigint", "int8", "bigserial"}, dataType)) {
if (arraysContains(new String[]{"bigint", "int8", "bigserial", "serial8"}, dataType)) {
return GenConstants.TYPE_LONG;
}
if (arraysContains(new String[]{"smallint", "mediumint", "int", "int2", "int4", "integer", "smallserial", "serial"}, dataType)) {
if (arraysContains(new String[]{"tinyint", "smallint", "mediumint", "int", "int2", "int4", "integer",
"smallserial", "serial", "serial2", "serial4"}, dataType)) {
return GenConstants.TYPE_INTEGER;
}
if (StringUtils.equals(dataType, "number")) {
@@ -169,6 +175,19 @@ public class GenUtils {
return GenConstants.TYPE_LONG;
}
/**
* 规范化数据库字段类型,去除 MySQL 等数据库的类型修饰。
*
* @param dataType 数据库字段类型
* @return 规范化后的数据库字段类型
*/
private static String normalizeDbType(String dataType) {
return StringUtils.trim(dataType)
.toLowerCase()
.replace(" unsigned", "")
.replace(" zerofill", "");
}
/**
* 根据整数字段长度解析 Java 类型。
*
@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.extra.template.TemplateConfig;
import cn.hutool.extra.template.TemplateEngine;
import cn.hutool.extra.template.TemplateUtil;
import cn.hutool.extra.template.engine.freemarker.FreemarkerEngine;
@@ -12,12 +13,10 @@ import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.DateUtils;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.mybatis.enums.DataBaseType;
import org.dromara.common.mybatis.helper.DataBaseHelper;
import org.dromara.gen.config.properties.GenProperties;
import org.dromara.gen.constant.GenConstants;
import org.dromara.gen.domain.GenTable;
import org.dromara.gen.domain.GenTableColumn;
@@ -27,6 +26,7 @@ import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.Consumer;
@@ -59,9 +59,13 @@ public class TemplateEngineUtils {
static {
// 模板引擎初始化
GenProperties properties = SpringUtils.getBean(GenProperties.class);
properties.getTemplateConfig().setCustomEngine(FreemarkerEngine.class);
TEMPLATE_ENGINE = TemplateUtil.createEngine(properties.getTemplateConfig());
TemplateConfig templateConfig = new TemplateConfig(StandardCharsets.UTF_8, StringUtils.EMPTY, TemplateConfig.ResourceMode.CLASSPATH);
templateConfig.setCustomEngine(FreemarkerEngine.class);
TEMPLATE_ENGINE = TemplateUtil.createEngine(templateConfig);
// 禁用数字千分位格式化 避免 Long 类型的 ID 渲染是被加上逗号
if (TEMPLATE_ENGINE instanceof FreemarkerEngine freemarkerEngine) {
freemarkerEngine.getConfiguration().setNumberFormat("computer");
}
TEMPLATE_MAPPER = PathNamedTemplate.form(TEMPLATE_ENGINE, GenConstants.TEMPLATE_PATHS);
}
@@ -168,6 +172,7 @@ public class TemplateEngineUtils {
boolean needDigit = false;
boolean needDateField = false;
boolean needSwitchField = false;
boolean needParseTime = false;
String firstTreeListField = StringUtils.EMPTY;
for (GenTableColumn column : genTable.getColumns()) {
boolean writable = column.isInsert() || column.isEdit();
@@ -183,6 +188,7 @@ public class TemplateEngineUtils {
needDigit = needDigit || writable && StringUtils.equals(column.getHtmlType(), GenConstants.HTML_INPUT_NUMBER);
needDateField = needDateField || writable && StringUtils.equals(column.getHtmlType(), GenConstants.HTML_DATETIME);
needSwitchField = needSwitchField || (writable || column.isList()) && StringUtils.equals(column.getHtmlType(), GenConstants.HTML_SWITCH);
needParseTime = needParseTime || column.isList() && StringUtils.equals(column.getHtmlType(), GenConstants.HTML_DATETIME);
if (StringUtils.isBlank(firstTreeListField) && column.isList()) {
firstTreeListField = column.getJavaField();
}
@@ -202,6 +208,7 @@ public class TemplateEngineUtils {
context.put("needDigit", needDigit);
context.put("needDateField", needDateField);
context.put("needSwitchField", needSwitchField);
context.put("needParseTime", needParseTime);
context.put("firstTreeListField", firstTreeListField);
}
@@ -351,6 +358,7 @@ public class TemplateEngineUtils {
String javaPath = PROJECT_PATH + "/" + StringUtils.replace(packageName, ".", "/");
String mybatisPath = MYBATIS_PATH + "/" + moduleName;
String frontendPath = getFrontendPath(genTable.getFrontendType());
String frontendPagePath = getFrontendPagePath(genTable.getFrontendType());
// templatePath
// genFilePathFormat
if (template.contains("domain.java.")) {
@@ -376,10 +384,10 @@ public class TemplateEngineUtils {
} else if (template.contains("types.ts.")) {
fileName = StringUtils.format("{}/api/{}/{}/types.ts", frontendPath, moduleName, businessName);
} else if (isFrontendPageTemplate(template, GenConstants.FRONTEND_INDEX_TEMPLATE_PREFIX)) {
fileName = StringUtils.format("{}/views/{}/{}/index.{}", frontendPath, moduleName, businessName,
fileName = StringUtils.format("{}/{}/{}/{}/index.{}", frontendPath, frontendPagePath, moduleName, businessName,
getFrontendPageExtension(template, GenConstants.FRONTEND_INDEX_TEMPLATE_PREFIX));
} else if (isFrontendPageTemplate(template, GenConstants.FRONTEND_INDEX_TREE_TEMPLATE_PREFIX)) {
fileName = StringUtils.format("{}/views/{}/{}/index.{}", frontendPath, moduleName, businessName,
fileName = StringUtils.format("{}/{}/{}/{}/index.{}", frontendPath, frontendPagePath, moduleName, businessName,
getFrontendPageExtension(template, GenConstants.FRONTEND_INDEX_TREE_TEMPLATE_PREFIX));
}
return fileName;
@@ -395,6 +403,16 @@ public class TemplateEngineUtils {
return resolveFrontendType(frontendType);
}
/**
* 获取前端页面输出目录。
*
* @param frontendType 前端模板类型
* @return 页面输出目录
*/
private static String getFrontendPagePath(String frontendType) {
return GenConstants.FRONTEND_TYPE_REACT.equals(resolveFrontendType(frontendType)) ? "pages" : "views";
}
/**
* 获取前端页面文件扩展名。
*
@@ -507,16 +525,26 @@ public class TemplateEngineUtils {
List<GenTableColumn> columns = genTable.getColumns();
HashSet<String> importList = new HashSet<>();
for (GenTableColumn column : columns) {
if (!column.isSuperColumn() && GenConstants.TYPE_DATE.equals(column.getJavaType())) {
if (needImportColumn(column) && GenConstants.TYPE_DATE.equals(column.getJavaType())) {
importList.add("java.time.LocalDateTime");
importList.add("com.fasterxml.jackson.annotation.JsonFormat");
} else if (!column.isSuperColumn() && GenConstants.TYPE_BIGDECIMAL.equals(column.getJavaType())) {
} else if (needImportColumn(column) && GenConstants.TYPE_BIGDECIMAL.equals(column.getJavaType())) {
importList.add("java.math.BigDecimal");
}
}
return importList;
}
/**
* 判断列是否需要生成类型导入。
*
* @param column 列对象
* @return 需要生成类型导入返回 {@code true}
*/
private static boolean needImportColumn(GenTableColumn column) {
return !column.isSuperColumn() || column.isQuery();
}
/**
* 根据列类型获取字典组
*
@@ -700,7 +728,16 @@ public class TemplateEngineUtils {
if (StringUtils.equals(column.getJavaType(), GenConstants.TYPE_LONG)) {
return value + "L";
}
if (StringUtils.equalsAny(column.getJavaType(), GenConstants.TYPE_INTEGER, GenConstants.TYPE_DOUBLE, GenConstants.TYPE_BIGDECIMAL)) {
if (StringUtils.equals(column.getJavaType(), GenConstants.TYPE_FLOAT)) {
return value + "F";
}
if (StringUtils.equals(column.getJavaType(), GenConstants.TYPE_DOUBLE)) {
return value + "D";
}
if (StringUtils.equals(column.getJavaType(), GenConstants.TYPE_BIGDECIMAL)) {
return "new java.math.BigDecimal(\"" + value + "\")";
}
if (StringUtils.equals(column.getJavaType(), GenConstants.TYPE_INTEGER)) {
return value;
}
return "\"" + value + "\"";
@@ -717,7 +754,8 @@ public class TemplateEngineUtils {
if (ObjectUtil.isNull(column) || StringUtils.isBlank(value)) {
return "undefined";
}
if (StringUtils.equalsAny(column.getJavaType(), GenConstants.TYPE_LONG, GenConstants.TYPE_INTEGER, GenConstants.TYPE_DOUBLE, GenConstants.TYPE_BIGDECIMAL)) {
if (StringUtils.equalsAny(column.getJavaType(), GenConstants.TYPE_LONG, GenConstants.TYPE_INTEGER,
GenConstants.TYPE_DOUBLE, GenConstants.TYPE_FLOAT, GenConstants.TYPE_BIGDECIMAL)) {
return value;
}
return "'" + value + "'";
@@ -30,7 +30,7 @@ public class ${ClassName}Bo implements Serializable {
private static final long serialVersionUID = 1L;
<#list columns as column>
<#if !table.isSuperColumn(column.javaField) && (column.query || column.insert || column.edit)>
<#if column.query || (!table.isSuperColumn(column.javaField) && (column.insert || column.edit))>
/**
* ${column.columnComment}
*/
@@ -41,7 +41,7 @@ public class ${ClassName}Bo implements Serializable {
<#elseif column.edit>
<#assign Group = "EditGroup.class">
</#if>
<#if column.required>
<#if column.required && !table.isSuperColumn(column.javaField) && (column.insert || column.edit)>
<#if column.javaType == 'String'>
@NotBlank(message = "${column.columnComment}不能为空", groups = { ${Group} })
<#else>
@@ -60,4 +60,3 @@ public class ${ClassName}Bo implements Serializable {
</#if>
}
@@ -59,10 +59,10 @@ public class ${ClassName}Controller extends BaseController {
}
</#if>
<#if enableExport>
/**
* 导出${functionName}列表
*/
<#if enableExport>
@SaCheckPermission("${permissionPrefix}:export")
@Log(title = "${functionName}", businessType = BusinessType.EXPORT)
@PostMapping("/export")
@@ -153,5 +153,3 @@ public class ${ClassName}Controller extends BaseController {
return toAjax(${className}Service.deleteWithValidByIds(List.of(${pkColumn.javaField}s), true));
}
}
@@ -46,5 +46,3 @@ public class ${ClassName} extends BaseEntity {
</#list>
}
@@ -13,6 +13,3 @@ import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
public interface ${ClassName}Mapper extends BaseMapperPlus<${ClassName}, ${ClassName}Vo> {
}
@@ -101,7 +101,5 @@ public interface I${ClassName}Service {
* @return 是否删除成功
*/
Boolean deleteWithValidByIds(Collection<${pkColumn.javaType}> ids, Boolean isValid);
}
@@ -43,7 +43,7 @@ public class ${ClassName}ServiceImpl implements I${ClassName}Service {
private final ${ClassName}Mapper ${className}Mapper;
/**
/**
* 查询${functionName}
*
* @param ${pkColumn.javaField} 主键
@@ -342,6 +342,5 @@ public class ${ClassName}ServiceImpl implements I${ClassName}Service {
}
return ${className}Mapper.deleteByIds(ids) > 0;
}
}
@@ -17,8 +17,6 @@ import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* ${functionName}视图对象 ${tableName}
*
@@ -44,7 +42,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>
@@ -61,5 +59,3 @@ public class ${ClassName}Vo implements Serializable {
</#list>
}
@@ -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>;
}
@@ -14,9 +14,7 @@ values(${table.menuIds[3]}, '${functionName}修改', ${table.menuIds[0]}, 3, '#
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[4]}, '${functionName}删除', ${table.menuIds[0]}, 4, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:remove', '#', 1761000000000000103, 1761100000000000001, sysdate(), null, null, '');
<#if enableExport>
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[5]}, '${functionName}导出', ${table.menuIds[0]}, 5, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:export', '#', 1761000000000000103, 1761100000000000001, sysdate(), null, null, '');
</#if>
@@ -14,9 +14,7 @@ values(${table.menuIds[3]}, '${functionName}修改', ${table.menuIds[0]}, 3, '#
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[4]}, '${functionName}删除', ${table.menuIds[0]}, 4, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:remove', '#', 1761000000000000103, 1761100000000000001, sysdate, null, null, '');
<#if enableExport>
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[5]}, '${functionName}导出', ${table.menuIds[0]}, 5, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:export', '#', 1761000000000000103, 1761100000000000001, sysdate, null, null, '');
</#if>
@@ -14,10 +14,7 @@ values(${table.menuIds[3]}, '${functionName}修改', ${table.menuIds[0]}, 3, '#
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[4]}, '${functionName}删除', ${table.menuIds[0]}, 4, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:remove', '#', 1761000000000000103, 1761100000000000001, now(), null, null, '');
<#if enableExport>
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[5]}, '${functionName}导出', ${table.menuIds[0]}, 5, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:export', '#', 1761000000000000103, 1761100000000000001, now(), null, null, '');
</#if>
@@ -14,9 +14,7 @@ values(${table.menuIds[3]}, '${functionName}修改', ${table.menuIds[0]}, 3, '#
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[4]}, '${functionName}删除', ${table.menuIds[0]}, 4, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:remove', '#', 1761000000000000103, 1761100000000000001, getdate(), null, null, '');
<#if enableExport>
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[5]}, '${functionName}导出', ${table.menuIds[0]}, 5, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:export', '#', 1761000000000000103, 1761100000000000001, getdate(), null, null, '');
</#if>
@@ -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';
@@ -8,8 +10,7 @@ import request from '@/utils/request';
* @param query
* @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',
@@ -98,6 +99,3 @@ export const del${BusinessName} = (${pkColumn.javaField}: string | number | Arra
method: 'delete'
});
};
@@ -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>
@@ -425,7 +425,7 @@ import { useTreeTableExpand } from '@/hooks/tree/useTreeTableExpand';
import { useDict } from '@/utils/dict';
</#if>
import modal from '@/plugins/modal';
import { handleTree } from '@/utils/ruoyi';
import { handleTree<#if needParseTime>, parseTime</#if> } from '@/utils/ruoyi';
<#if enableExport>
import { download as requestDownload } from '@/utils/request';
</#if>
@@ -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>
@@ -4,6 +4,3 @@
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="${packageName}.mapper.${ClassName}Mapper">
</mapper>
@@ -8,9 +8,3 @@ gen:
autoRemovePre: false
# 表前缀(生成类名不会包含表前缀,多个用逗号分隔)
tablePrefix: sys_
# 模板配置
templateConfig:
# 模版加载模式
resourceMode: CLASSPATH
# 模板加载路径
# path: fm
@@ -60,11 +60,16 @@ public class ProfileUserVo implements Serializable {
*/
private String gender;
/**
* 头像 OSS ID
*/
private Long avatar;
/**
* 头像地址
*/
@Translation(type = TransConstant.OSS_ID_TO_URL)
private Long avatar;
@Translation(type = TransConstant.OSS_ID_TO_URL, mapper = "avatar")
private String avatarUrl;
/**
* 最后登录IP
@@ -70,11 +70,16 @@ public class SysUserVo implements Serializable {
*/
private String gender;
/**
* 头像 OSS ID
*/
private Long avatar;
/**
* 头像地址
*/
@Translation(type = TransConstant.OSS_ID_TO_URL)
private Long avatar;
@Translation(type = TransConstant.OSS_ID_TO_URL, mapper = "avatar")
private String avatarUrl;
/**
* 密码
@@ -219,9 +219,9 @@ public class SysClientServiceImpl implements ISysClientService {
* @return 逗号拼接后的规则串
*/
private String resolveRuleValue(String rawValue, List<String> listValue, UnaryOperator<String> normalizer) {
List<String> rules = CollUtil.isNotEmpty(listValue)
? listValue
: StringUtils.str2List(rawValue, CLIENT_RULE_SEPARATOR_REGEX, true, true);
List<String> rules = rawValue != null
? StringUtils.str2List(rawValue, CLIENT_RULE_SEPARATOR_REGEX, true, true)
: listValue;
if (CollUtil.isEmpty(rules)) {
return listValue != null || rawValue != null ? "" : null;
}
@@ -4,19 +4,16 @@ import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.domain.PageResult;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.ip.AddressUtils;
import org.dromara.common.log.event.LoginInfoEvent;
import org.dromara.common.mybatis.core.mapper.LambdaCrudChainWrapper;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.system.domain.SysLoginInfo;
import org.dromara.system.domain.bo.SysLoginInfoBo;
import org.dromara.system.domain.vo.SysClientVo;
@@ -55,11 +52,10 @@ public class SysLoginInfoServiceImpl implements ISysLoginInfoService {
@Async
@EventListener
public void recordLoginInfo(LoginInfoEvent loginInfoEvent) {
HttpServletRequest request = loginInfoEvent.getRequest();
UserAgent userAgent = UserAgentUtil.parse(request.getHeader("User-Agent"));
String ip = ServletUtils.getClientIP(request);
UserAgent userAgent = UserAgentUtil.parse(loginInfoEvent.getUserAgent());
String ip = loginInfoEvent.getIp();
// 客户端信息
String clientId = request.getHeader(LoginHelper.CLIENT_KEY);
String clientId = loginInfoEvent.getClientId();
SysClientVo client = null;
if (StringUtils.isNotBlank(clientId)) {
client = clientService.queryByClientId(clientId);

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