mirror of
https://github.com/dromara/RuoYi-Vue-Plus.git
synced 2026-08-27 05:47:14 +00:00
Compare commits
67 Commits
v6.0.0-BETA
...
6.X
| Author | SHA1 | Date | |
|---|---|---|---|
| 13e8ac5f85 | |||
| b37db3fd01 | |||
| 629e344af5 | |||
| 2933badb91 | |||
| 387c4f0a20 | |||
| a16f249024 | |||
| b9538b0d1c | |||
| 1df89481db | |||
| 04ef4b1147 | |||
| 430478c040 | |||
| 70deffdf05 | |||
| fba7b40075 | |||
| 6d8c0d6d47 | |||
| d31082f4a7 | |||
| c76212003c | |||
| 41ea764a8c | |||
| 7180b52977 | |||
| 070d0bdd45 | |||
| a2e3eda278 | |||
| ac61c0a558 | |||
| a562c78d27 | |||
| 071db648cd | |||
| 47a71acf49 | |||
| 46cef1c6c7 | |||
| d6760f93d5 | |||
| da5f30cae2 | |||
| 28c39d5b90 | |||
| d3146a5755 | |||
| e8f2e68f0e | |||
| c2667cfbe1 | |||
| 44a9bd1006 | |||
| 543037885d | |||
| d7eca3db85 | |||
| 348141427d | |||
| 4baa9edfa1 | |||
| a5b65da892 | |||
| 126380e9f5 | |||
| 23387d71d8 | |||
| 59f1e75bdc | |||
| c4b1e58c3e | |||
| d9894b534d | |||
| c8f755cbde | |||
| 53f857714d | |||
| 6876bd355f | |||
| 7ca18b7798 | |||
| dbbc790081 | |||
| 4e6dbee008 | |||
| e571f339b4 | |||
| 74ed2734f0 | |||
| 1fbe1694e4 | |||
| c56dc6b8e9 | |||
| 1be4f7b07e | |||
| 5392c3228e | |||
| 837604f889 | |||
| eab4228548 | |||
| e8b033c4ef | |||
| 3cf2eceb13 | |||
| 1d263f7656 | |||
| c1e344be0e | |||
| 612a965c26 | |||
| d7c525d7f8 | |||
| 93a07e57b8 | |||
| 642c713f78 | |||
| 123ba9bfba | |||
| 66b4840356 | |||
| 14e2cf16cc | |||
| f3256442c8 |
@@ -61,4 +61,4 @@ description: 标准后端 CRUD 专家。用于当前项目中的新增单表 CRU
|
||||
- BO / VO / Entity 是否职责分离
|
||||
- 导出、分页、删除前校验是否齐全
|
||||
- 是否只是 generator 裸产物,如果是要继续补齐项目约定
|
||||
- 前端 `api/types/index.vue` 如需同步,接口路径、返回结构、日期范围参数要与后端一致
|
||||
- 前端 `api/types` 和 Vue `index.vue` 或 React `index.tsx` 如需同步,接口路径、返回结构、日期范围参数要与后端一致
|
||||
|
||||
@@ -12,12 +12,12 @@ description: 后端工程总入口。用于在当前 RuoYi-Vue-Plus 项目中识
|
||||
3. 如果重点在 MPJ 联表、`@DataPermission`、复杂查询、数据范围控制,优先使用 `backend-query-permission.md` 的规则。
|
||||
4. 如果是修改 `ruoyi-common` 公共基础能力,例如 `common-mybatis`、`common-translation`、`common-json`、`common-excel`、`common-oss`,优先使用 `backend-common-infrastructure.md` 的规则。
|
||||
5. 如果只要求补充或修正 JavaDoc 注释,优先使用 `backend-javadoc.md` 的规则。
|
||||
6. 如果同时要求同步前端接口或前端页面骨架,保持后端路由与 generator 风格稳定,便于前端 agent 对接。
|
||||
6. 如果同时要求同步前端接口或前端页面骨架,先确认目标前端是 Vue 还是 React,保持后端路由与 generator 风格稳定,便于前端 agent 对接。
|
||||
|
||||
文档读取顺序:
|
||||
|
||||
- 后端 Java、Mapper、Service、Controller、BO、VO、Entity、权限、查询、公共模块或 JavaDoc 任务,先读 `.codex/skills/ruoyi-plus-ai-coding/references/backend.md`。
|
||||
- 同步前端 Vue、TypeScript、api、types 或页面骨架时,再读 `.codex/skills/ruoyi-plus-ai-coding/references/frontend.md`。
|
||||
- 同步前端 Vue、React、TypeScript、api、types 或页面骨架时,再读 `.codex/skills/ruoyi-plus-ai-coding/references/frontend.md`。
|
||||
- 任务边界不清晰或需要标准场景示例时,再读 `.codex/skills/ruoyi-plus-ai-coding/references/examples.md`。
|
||||
- 只读取当前任务相关的 reference,不一次性展开全部文档。
|
||||
- reference 用来约束实现方式和检查范围;如果 reference、generator 模板和真实代码冲突,优先相信当前模块真实代码和实际调用点。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: ruoyi-plus-ai-coding
|
||||
description: 在仓库内按代码生成器模板、项目 reference 文档和既有约定生成或修改代码。用于新增或修改 CRUD 模块、controller/service/mapper/BO/VO/entity、MyBatis-Plus/MPJ 查询、数据权限、缓存、翻译/JSON 增强、公共 common 模块能力、JavaDoc 注释,以及与后端接口配套的 Vue 3 + TypeScript 页面、types 和 api 文件;触发后应先按任务类型读取对应 references,再阅读目标模块真实代码和 generator 模板。
|
||||
description: 在仓库内按代码生成器模板、项目 reference 文档和既有约定生成或修改代码。用于新增或修改 CRUD 模块、controller/service/mapper/BO/VO/entity、MyBatis-Plus/MPJ 查询、数据权限、缓存、翻译/JSON 增强、公共 common 模块能力、JavaDoc 注释,以及与后端接口配套的 Vue 或 React 前端页面、types 和 api 文件;触发后应先按任务类型读取对应 references,再阅读目标模块真实代码和 generator 模板。
|
||||
---
|
||||
|
||||
# RuoYi Plus AI 编码规范
|
||||
@@ -17,7 +17,7 @@ description: 在仓库内按代码生成器模板、项目 reference 文档和
|
||||
- 修改 `ruoyi-common` 公共能力,例如 mybatis 查询构造器、translation、json enhance、excel、oss、redis、web 配置。
|
||||
- 补充或修正 JavaDoc 注释,尤其是公共 API、接口、BO/VO/Entity 字段、Mapper 默认方法、Service/Controller 方法。
|
||||
- 在系统、监控、工作流、demo 等模块内按现有约定扩展业务代码。
|
||||
- 为后端新增接口同步补前端 `api/types/index.vue` 骨架。
|
||||
- 为后端新增接口同步补前端 `api/types` 和 Vue `index.vue` 或 React `index.tsx` 页面骨架。
|
||||
|
||||
## 不适用场景
|
||||
|
||||
@@ -45,7 +45,7 @@ description: 在仓库内按代码生成器模板、项目 reference 文档和
|
||||
使用本 skill 时,先按任务类型读取适用 reference,不一次性展开所有文档:
|
||||
|
||||
- 后端 Java、Mapper、Service、Controller、BO、VO、Entity、权限、查询、公共模块或 JavaDoc 任务,先读 [references/backend.md](references/backend.md)。
|
||||
- 前端 Vue、TypeScript、api、types 或页面任务,先读 [references/frontend.md](references/frontend.md)。
|
||||
- 前端 Vue、React、TypeScript、api、types 或页面任务,先读 [references/frontend.md](references/frontend.md)。
|
||||
- 不确定任务边界、需要标准调用方式或需要对照典型场景时,再读 [references/examples.md](references/examples.md)。
|
||||
|
||||
reference 用来约束实现方式和自检范围;发生冲突时,仍以当前模块真实代码和实际调用点为准。
|
||||
@@ -71,7 +71,7 @@ Java、MyBatis-Plus、BO/VO/entity、controller、mapper、service 的具体规
|
||||
|
||||
## 前端规则
|
||||
|
||||
Vue 3、TypeScript API 文件、生成式列表页、表单状态、字典和日期范围约定见 [references/frontend.md](references/frontend.md)。
|
||||
Vue 3、React、TypeScript API 文件、生成式列表页、表单状态、字典和日期范围约定见 [references/frontend.md](references/frontend.md)。
|
||||
|
||||
## 使用案例
|
||||
|
||||
@@ -163,7 +163,7 @@ Vue 3、TypeScript API 文件、生成式列表页、表单状态、字典和日
|
||||
- BO 使用 `@AutoMapper(target = Entity.class, reverseConvertGenerate = false)`。
|
||||
- VO 使用 `@AutoMapper(target = Entity.class)`。
|
||||
- 前端 API 路径与后端路由完全对应。
|
||||
- 前端列表页继续使用仓库里的 `proxy?.addDateRange`、`proxy?.$modal`、`proxy?.download`、`useDict`、`pagination` 等工具。
|
||||
- 前端列表页继续使用对应前端工程已有工具:Vue 侧如 `proxy?.addDateRange`、`proxy?.$modal`、`proxy?.download`、`useDict`、`pagination`;React 侧如 `ProTable`、`ModalForm`、`useTableSelection`、`useDateRangeQuery`、`useTableExport`。
|
||||
|
||||
## 推荐提问方式
|
||||
|
||||
|
||||
@@ -62,13 +62,13 @@
|
||||
### 用户提问示例
|
||||
|
||||
```text
|
||||
使用 $ruoyi-plus-ai-coding 为 monitor/cache 新增一个导出接口,并同步补齐前端 api/types 调用骨架。
|
||||
使用 $ruoyi-plus-ai-coding 为 monitor/cache 新增一个导出接口,并同步补齐 Vue 或 React 前端 api/types 调用骨架。
|
||||
```
|
||||
|
||||
### 期望执行方式
|
||||
|
||||
- 先补后端 `controller/service`。
|
||||
- 再根据后端路由补前端 `src/api` 或 generator 风格的前端骨架。
|
||||
- 再根据后端路由和目标前端类型补 `src/api` 或 generator 风格的前端骨架。
|
||||
- 保证导出接口路径和前端下载调用一致。
|
||||
|
||||
## 案例 6:推荐的高质量任务描述
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
- 默认 Vue 模板在 `fm/vue`,React 模板在 `fm/react`
|
||||
- 前端工程中与目标模块最接近的现有页面
|
||||
|
||||
当前 boot4 仓库通常只含后端与 generator 前端模板;如果前端工程不在当前 root,先以 generator 模板约定为准,再对照用户提供的前端目录或相邻仓库。
|
||||
当前 boot4 仓库通常只含后端与 generator 前端模板;如果前端工程不在当前仓库根目录,先以 generator 模板约定为准,再对照用户提供的前端工程或官方前端分支:
|
||||
|
||||
- Vue 前端:`https://gitee.com/JavaLionLi/plus-ui/tree/6.X-Vue`
|
||||
- React 前端:`https://gitee.com/JavaLionLi/plus-ui/tree/6.X-React`
|
||||
|
||||
## 前端模板选择规则
|
||||
|
||||
@@ -17,11 +20,10 @@
|
||||
|
||||
## API 文件规则
|
||||
|
||||
- 从 `@/utils/request` 引入 `request`。
|
||||
- 从 `@/utils/api-types` 引入 `AxiosPromise`。
|
||||
- 从 `@/api/types` 引入 `PageResult`。
|
||||
- 从 `@/api/<module>/<business>/types` 引入本模块类型。
|
||||
- 列表接口通常返回 `AxiosPromise<PageResult<Vo>>`。
|
||||
- Vue 模板从 `@/utils/request` 引入 `request`,从 `@/utils/api-types` 引入 `AxiosPromise`,从 `@/api/types` 引入 `PageResult`。
|
||||
- React 模板从 `@/api/request` 引入 `request`,从 `@/api/types` 引入 `R`、`PageResult`。
|
||||
- 本模块类型:Vue 模板从 `@/api/<module>/<business>/types` 引入,React 模板从 `./types` 引入。
|
||||
- Vue 列表接口通常返回 `AxiosPromise<PageResult<Vo>>`;React 列表接口通常返回 `request<R<PageResult<Vo>>>(...)`。
|
||||
- 常规接口命名和路由保持:
|
||||
`listXxx` -> `GET /<module>/<business>/list`
|
||||
`getXxx` -> `GET /<module>/<business>/{id}`
|
||||
@@ -38,7 +40,7 @@
|
||||
- Java 数值类型通常映射为 `number`。
|
||||
- Boolean 映射为 `boolean`。
|
||||
- 其他生成字段默认多为 `string`。
|
||||
- 存在日期范围查询时保留 `params?: any`。
|
||||
- 存在日期范围查询时保留 `params`:Vue 模板通常是 `params?: any`,React 模板通常是 `params?: Record<string, unknown>`。
|
||||
|
||||
## Vue 页面规则
|
||||
|
||||
@@ -51,7 +53,19 @@
|
||||
- 弹窗状态优先由 `useFormDialog` 返回的 `dialog`、`openDialog`、`showDialog`、`closeDialog` 管理。
|
||||
- 表单引用通常命名为 `queryFormRef` 和 `<business>FormRef`。
|
||||
|
||||
## 页面行为规则
|
||||
## React 页面规则
|
||||
|
||||
- 使用 `index.tsx`,组件默认导出 `<BusinessName>Page`。
|
||||
- 页面主体优先沿用 Ant Design Pro:`PageContainer`、`ProTable`、`ModalForm`、`ProColumns`、`ActionType`。
|
||||
- 表单优先使用 `Form.useForm<Form>()`,弹窗开关优先使用 `ahooks` 的 `useBoolean`。
|
||||
- 权限通过 `useUserStore` 取 `userInfo`,再用 `hasPermi(userInfo, ['module:business:action'])` 生成 `canAdd`、`canEdit`、`canRemove`、`canExport`。
|
||||
- 表格选择使用 `useTableSelection<VO>(row => row.id)`;表格刷新使用 `actionRef.current?.reload()` 或 `reloadAndRest?.()`。
|
||||
- 字典使用 `useDict` 和 `dictOptions`,展示使用 `DictTag`。
|
||||
- 日期范围使用 `useDateRangeQuery`,在 `ProTable` 的 `request` 中由 `toPageQuery(params)` 转查询参数后再应用范围字段。
|
||||
- 导出使用 `useTableExport`,路径保持 `/<module>/<business>/export`。
|
||||
- 文件、图片、富文本组件使用 React 工程已有的 `FileUpload`、`ImageUpload`、`ImagePreview`、`RichTextEditor`。
|
||||
|
||||
## Vue 页面行为规则
|
||||
|
||||
- `getList` 负责通过 `withLoading` 设置 loading、处理日期范围参数、调用列表接口、回填 `rows` 和 `total`。
|
||||
- `handleQuery` 通常先把 `pageNum` 重置为 `1`,再重新查询。
|
||||
@@ -63,13 +77,20 @@
|
||||
- `handleDelete` 使用 `modal.confirm(...)` 确认,再调用删除接口并刷新。
|
||||
- `handleExport` 使用 `download as requestDownload` 从 `@/utils/request` 导出的下载方法。
|
||||
|
||||
## React 页面行为规则
|
||||
|
||||
- React `ProTable` 页面通过 `request` 回调加载列表并返回 `toTableData(res)`;新增、修改、删除成功后调用 `actionRef` 刷新。
|
||||
- React 弹窗提交函数根据主键判断调用新增还是更新,成功后 `message.success('操作成功')` 并重置表单。
|
||||
|
||||
## 模板结构规则
|
||||
|
||||
- 优先保持生成器的页面布局结构:搜索区卡片、表格区卡片、工具栏、分页、弹窗表单。
|
||||
- 保留 `v-hasPermi="['module:business:add']"` 这类权限指令。
|
||||
- 继续使用仓库已有组件:`right-toolbar`、`pagination`、`dict-tag`、`image-preview`、`image-upload`、`file-upload`、`editor`。
|
||||
- 优先保持生成器的页面布局结构,不在 Vue 和 React 之间互相移植组件体系。
|
||||
- Vue 保留 `v-hasPermi="['module:business:add']"` 这类权限指令。
|
||||
- Vue 继续使用仓库已有组件:`right-toolbar`、`pagination`、`dict-tag`、`image-preview`、`image-upload`、`file-upload`、`editor`。
|
||||
- React 继续使用仓库已有组件:`RowActions`、`DictTag`、`ImagePreview`、`ImageUpload`、`FileUpload`、`RichTextEditor`。
|
||||
- 已有页面对时间列使用 `parseTime` 时,新页面保持一致。
|
||||
- BETWEEN 日期查询继续使用 `el-date-picker`,脚本侧通过 `useDateRangeQuery` 生成 `dateRangeXxx`、`applyXxxDateRange`、`resetXxxDateRange`。
|
||||
- Vue BETWEEN 日期查询继续使用 `el-date-picker`,脚本侧通过 `useDateRangeQuery` 生成 `dateRangeXxx`、`applyXxxDateRange`、`resetXxxDateRange`。
|
||||
- React BETWEEN 日期查询继续使用 `ProTable` 的 `dateTimeRange` 搜索列,查询侧通过 `useDateRangeQuery` 写入 `params`。
|
||||
|
||||
## 避免事项
|
||||
|
||||
@@ -77,3 +98,4 @@
|
||||
- 模块已使用字典时,不要把选项文案硬编码到页面里。
|
||||
- 不要让 API 函数名和路由段偏离后端约定。
|
||||
- 后端 BO/service 依赖 begin/end 参数时,不要从查询对象里删掉 `params` 和日期范围处理。
|
||||
- 不要把 Vue 的 `proxy`、`v-hasPermi`、Element Plus 组件写进 React 页面,也不要把 React 的 `ProTable`、`ModalForm`、Ant Design 权限判断写进 Vue 页面。
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
|
||||
FROM bellsoft/liberica-openjdk-rocky:21.0.11-cds
|
||||
# FROM bellsoft/liberica-openjdk-rocky:25.0.3-cds
|
||||
FROM bellsoft/liberica-openjdk-rocky:21.0.12-cds
|
||||
# FROM bellsoft/liberica-openjdk-rocky:25.0.4-cds
|
||||
# FROM findepi/graalvm:java21-native
|
||||
|
||||
LABEL maintainer="Lion Li"
|
||||
|
||||
@@ -143,6 +143,10 @@
|
||||
<artifactId>ruoyi-gen</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<activation>
|
||||
<!-- 默认激活 -->
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
|
||||
@@ -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
|
||||
# 是否锁定当前智能体
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
<module>ruoyi-common-json</module>
|
||||
<module>ruoyi-common-encrypt</module>
|
||||
<module>ruoyi-common-push</module>
|
||||
<module>ruoyi-common-liteflow</module>
|
||||
<module>ruoyi-common-mqtt</module>
|
||||
<module>ruoyi-common-ai</module>
|
||||
<module>ruoyi-common-mcp</module>
|
||||
|
||||
-124
@@ -1,124 +0,0 @@
|
||||
package org.dromara.common.ai.handler;
|
||||
|
||||
import com.aizuda.snail.ai.agent.chat.starter.SnailAiChatGatewayController;
|
||||
import com.aizuda.snail.ai.common.execption.BaseSnailAiException;
|
||||
import com.aizuda.snail.ai.common.execption.SnailAiAuthenticationException;
|
||||
import com.aizuda.snail.ai.common.model.ModelCallException;
|
||||
import com.aizuda.snail.ai.common.model.Result;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.MessageSourceResolvable;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.method.annotation.HandlerMethodValidationException;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Snail AI Chat 网关异常处理器。
|
||||
* <p>
|
||||
* /api/snail/chat/** 接口由 Snail AI SDK 前端消费,响应结构需要保持 SDK Result(status/message/data),
|
||||
* 不能落到 RuoYi 通用 R(code/msg/data) 格式。
|
||||
*/
|
||||
@Slf4j
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
@RestControllerAdvice(assignableTypes = SnailAiChatGatewayController.class)
|
||||
public class SnailAiChatExceptionHandler {
|
||||
|
||||
private static final int AUTHENTICATION_ERROR_STATUS = 5001;
|
||||
|
||||
@ExceptionHandler(SnailAiAuthenticationException.class)
|
||||
public Result<Void> handleAuthenticationException(SnailAiAuthenticationException e) {
|
||||
log.warn("Snail AI Chat authentication failed: {}", e.getMessage());
|
||||
return Result.fail(AUTHENTICATION_ERROR_STATUS, defaultMessage(e, "认证失败,请重新登录"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(BaseSnailAiException.class)
|
||||
public Result<Void> handleSnailAiException(BaseSnailAiException e) {
|
||||
log.warn("Snail AI Chat request failed: {}", e.getMessage());
|
||||
return Result.fail(defaultMessage(e, "AI 服务请求失败,请稍后再试"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(ModelCallException.class)
|
||||
public Result<Void> handleModelCallException(ModelCallException e) {
|
||||
log.warn("Snail AI Chat model call failed: {}", e.getMessage(), e);
|
||||
return Result.fail(defaultMessage(e, "模型调用失败,请稍后再试"));
|
||||
}
|
||||
|
||||
@ExceptionHandler({
|
||||
BindException.class,
|
||||
MethodArgumentNotValidException.class,
|
||||
HandlerMethodValidationException.class
|
||||
})
|
||||
public Result<Void> handleValidationException(Exception e) {
|
||||
log.warn("Snail AI Chat validation failed: {}", e.getMessage());
|
||||
return Result.fail(validationMessage(e));
|
||||
}
|
||||
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
public Result<Void> handleConstraintViolationException(ConstraintViolationException e) {
|
||||
log.warn("Snail AI Chat constraint validation failed: {}", e.getMessage());
|
||||
String message = e.getConstraintViolations().stream()
|
||||
.map(ConstraintViolation::getMessage)
|
||||
.filter(this::hasText)
|
||||
.collect(Collectors.joining(", "));
|
||||
return Result.fail(hasText(message) ? message : "请求参数校验失败");
|
||||
}
|
||||
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
public Result<Void> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
|
||||
log.warn("Snail AI Chat request body parse failed: {}", e.getMessage());
|
||||
return Result.fail("请求参数格式错误");
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public Result<Void> handleIllegalArgumentException(IllegalArgumentException e) {
|
||||
log.warn("Snail AI Chat illegal argument: {}", e.getMessage());
|
||||
return Result.fail(defaultMessage(e, "请求参数不合法"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalStateException.class)
|
||||
public Result<Void> handleIllegalStateException(IllegalStateException e) {
|
||||
log.warn("Snail AI Chat illegal state: {}", e.getMessage());
|
||||
return Result.fail(defaultMessage(e, "AI 会话状态异常,请刷新后重试"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public Result<Void> handleException(Exception e) {
|
||||
log.error("Snail AI Chat unexpected exception", e);
|
||||
return Result.fail("AI 服务异常,请稍后再试");
|
||||
}
|
||||
|
||||
private String validationMessage(Exception e) {
|
||||
Collection<? extends MessageSourceResolvable> errors;
|
||||
if (e instanceof BindException bindException) {
|
||||
errors = bindException.getAllErrors();
|
||||
} else if (e instanceof MethodArgumentNotValidException methodArgumentNotValidException) {
|
||||
errors = methodArgumentNotValidException.getBindingResult().getAllErrors();
|
||||
} else if (e instanceof HandlerMethodValidationException handlerMethodValidationException) {
|
||||
errors = handlerMethodValidationException.getAllErrors();
|
||||
} else {
|
||||
return "请求参数校验失败";
|
||||
}
|
||||
String message = errors.stream()
|
||||
.map(MessageSourceResolvable::getDefaultMessage)
|
||||
.filter(this::hasText)
|
||||
.collect(Collectors.joining(", "));
|
||||
return hasText(message) ? message : "请求参数校验失败";
|
||||
}
|
||||
|
||||
private String defaultMessage(Throwable e, String fallback) {
|
||||
return hasText(e.getMessage()) ? e.getMessage() : fallback;
|
||||
}
|
||||
|
||||
private boolean hasText(String value) {
|
||||
return value != null && !value.isBlank();
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
</description>
|
||||
|
||||
<properties>
|
||||
<revision>6.0.0-BETA</revision>
|
||||
<revision>6.0.0</revision>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
@@ -159,6 +159,13 @@
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- liteflow模块 -->
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-liteflow</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- mqtt模块 -->
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ import org.dromara.common.doc.core.customizer.JavadocOperationCustomizer;
|
||||
import org.dromara.common.doc.core.resolver.JavadocResolver;
|
||||
import org.dromara.common.doc.core.resolver.SaTokenAnnotationMetadataJavadocResolver;
|
||||
import org.springdoc.core.configuration.SpringDocConfiguration;
|
||||
import org.springdoc.core.customizers.OpenApiCustomizer;
|
||||
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
|
||||
import org.springdoc.core.providers.JavadocProvider;
|
||||
import org.springdoc.core.utils.PropertyResolverUtils;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
@@ -108,7 +108,7 @@ public class SpringDocConfig {
|
||||
* 对已经生成好的 OpenApi 进行自定义操作
|
||||
*/
|
||||
@Bean
|
||||
public OpenApiCustomizer openApiCustomizer() {
|
||||
public GlobalOpenApiCustomizer openApiCustomizer() {
|
||||
String contextPath = serverProperties.getServlet().getContextPath();
|
||||
String finalContextPath;
|
||||
if (StringUtils.isBlank(contextPath) || "/".equals(contextPath)) {
|
||||
|
||||
+3
-3
@@ -8,8 +8,8 @@ import io.swagger.v3.oas.models.Operation;
|
||||
import io.swagger.v3.oas.models.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springdoc.core.customizers.OpenApiCustomizer;
|
||||
import org.springdoc.core.customizers.OperationCustomizer;
|
||||
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
|
||||
import org.springdoc.core.customizers.GlobalOperationCustomizer;
|
||||
import org.springdoc.core.providers.JavadocProvider;
|
||||
import org.springdoc.core.service.OpenAPIService;
|
||||
import org.springdoc.core.utils.PropertyResolverUtils;
|
||||
@@ -29,7 +29,7 @@ import java.util.stream.Stream;
|
||||
* @author Lion Li
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class ClassTagOperationCustomizer implements OperationCustomizer, OpenApiCustomizer {
|
||||
public class ClassTagOperationCustomizer implements GlobalOperationCustomizer, GlobalOpenApiCustomizer {
|
||||
|
||||
/**
|
||||
* JavaDoc 提供器。
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ import io.swagger.v3.oas.models.Operation;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.dromara.common.doc.core.resolver.JavadocResolver;
|
||||
import org.springdoc.core.customizers.OperationCustomizer;
|
||||
import org.springdoc.core.customizers.GlobalOperationCustomizer;
|
||||
import org.springdoc.core.providers.JavadocProvider;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
@@ -18,7 +18,7 @@ import java.util.Optional;
|
||||
* @author Lion Li
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class JavadocOperationCustomizer implements OperationCustomizer {
|
||||
public class JavadocOperationCustomizer implements GlobalOperationCustomizer {
|
||||
|
||||
/**
|
||||
* JavaDoc 提供器。
|
||||
|
||||
+5
-21
@@ -23,11 +23,11 @@ import java.util.Base64;
|
||||
public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
|
||||
|
||||
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||
private static final Charset RESPONSE_CHARSET = StandardCharsets.UTF_8;
|
||||
|
||||
private final ByteArrayOutputStream byteArrayOutputStream;
|
||||
private final ServletOutputStream servletOutputStream;
|
||||
private PrintWriter printWriter;
|
||||
private Charset charset;
|
||||
|
||||
/**
|
||||
* 构造加密响应包装器。
|
||||
@@ -39,7 +39,6 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
|
||||
super(response);
|
||||
this.byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
this.servletOutputStream = this.getOutputStream();
|
||||
this.charset = resolveCharset(response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,8 +49,7 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
|
||||
@Override
|
||||
public PrintWriter getWriter() {
|
||||
if (printWriter == null) {
|
||||
charset = resolveCharset((HttpServletResponse) getResponse());
|
||||
printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream, charset));
|
||||
printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream, RESPONSE_CHARSET));
|
||||
}
|
||||
return printWriter;
|
||||
}
|
||||
@@ -106,7 +104,7 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
|
||||
*/
|
||||
public String getContent() throws IOException {
|
||||
flushBuffer();
|
||||
return byteArrayOutputStream.toString(charset);
|
||||
return byteArrayOutputStream.toString(RESPONSE_CHARSET);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,14 +128,14 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
|
||||
// vue版本需要设置
|
||||
servletResponse.addHeader("Access-Control-Expose-Headers", headerFlag);
|
||||
servletResponse.setHeader(headerFlag, encryptPassword);
|
||||
servletResponse.setCharacterEncoding(charset.name());
|
||||
servletResponse.setCharacterEncoding(RESPONSE_CHARSET.name());
|
||||
|
||||
|
||||
// 获取原始内容
|
||||
String originalBody = this.getContent();
|
||||
// 对内容进行加密
|
||||
String encryptContent = EncryptUtils.encryptByAes(originalBody, aesPassword);
|
||||
servletResponse.setContentLengthLong(encryptContent.getBytes(charset).length);
|
||||
servletResponse.setContentLengthLong(encryptContent.getBytes(RESPONSE_CHARSET).length);
|
||||
return encryptContent;
|
||||
}
|
||||
|
||||
@@ -206,20 +204,6 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析响应字符集,未设置时默认使用 UTF-8。
|
||||
*
|
||||
* @param response 原始响应
|
||||
* @return 响应字符集
|
||||
*/
|
||||
private Charset resolveCharset(HttpServletResponse response) {
|
||||
String characterEncoding = response.getCharacterEncoding();
|
||||
if (characterEncoding == null) {
|
||||
return StandardCharsets.UTF_8;
|
||||
}
|
||||
return Charset.forName(characterEncoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成响应内容 AES 加密密钥。
|
||||
*
|
||||
|
||||
+58
-5
@@ -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>
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package org.dromara.common.liteflow.component;
|
||||
|
||||
import com.yomahub.liteflow.annotation.LiteflowComponent;
|
||||
import com.yomahub.liteflow.core.NodeBooleanComponent;
|
||||
|
||||
/**
|
||||
* LiteFlow 恒为 false 的条件节点。
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@LiteflowComponent("alwaysFalse")
|
||||
public class AlwaysFalseComponent extends NodeBooleanComponent {
|
||||
|
||||
@Override
|
||||
public boolean processBoolean() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package org.dromara.common.liteflow.component;
|
||||
|
||||
import com.yomahub.liteflow.annotation.LiteflowComponent;
|
||||
import com.yomahub.liteflow.core.NodeBooleanComponent;
|
||||
|
||||
/**
|
||||
* LiteFlow 恒为 true 的条件节点。
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@LiteflowComponent("alwaysTrue")
|
||||
public class AlwaysTrueComponent extends NodeBooleanComponent {
|
||||
|
||||
@Override
|
||||
public boolean processBoolean() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package org.dromara.common.liteflow.component;
|
||||
|
||||
import com.yomahub.liteflow.annotation.LiteflowComponent;
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
|
||||
/**
|
||||
* LiteFlow 上下文必填校验节点。
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@LiteflowComponent("contextRequired")
|
||||
public class ContextRequiredComponent extends NodeComponent {
|
||||
|
||||
@Override
|
||||
public void process() {
|
||||
if (getFirstContextBean() == null) {
|
||||
throw new ServiceException("LiteFlow 上下文不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package org.dromara.common.liteflow.component;
|
||||
|
||||
import com.yomahub.liteflow.annotation.LiteflowComponent;
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.liteflow.core.FailMessageProvider;
|
||||
|
||||
/**
|
||||
* LiteFlow 失败节点,用于显式表达链路分支不可继续执行。
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@LiteflowComponent("fail")
|
||||
public class FailComponent extends NodeComponent {
|
||||
|
||||
private static final String DEFAULT_MESSAGE = "LiteFlow 链路执行失败";
|
||||
|
||||
@Override
|
||||
public void process() {
|
||||
throw new ServiceException(getFailMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 优先从上下文读取业务失败提示,未提供时使用默认提示。
|
||||
*
|
||||
* @return 失败提示
|
||||
*/
|
||||
private String getFailMessage() {
|
||||
Object context = getFirstContextBean();
|
||||
if (context instanceof FailMessageProvider provider) {
|
||||
String message = provider.getFailMessage();
|
||||
if (StringUtils.isNotBlank(message)) {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
return DEFAULT_MESSAGE;
|
||||
}
|
||||
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package org.dromara.common.liteflow.component;
|
||||
|
||||
import com.yomahub.liteflow.annotation.LiteflowComponent;
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
|
||||
/**
|
||||
* LiteFlow 空节点,用于显式表达无需处理的分支。
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@LiteflowComponent("noop")
|
||||
public class NoopComponent extends NodeComponent {
|
||||
|
||||
@Override
|
||||
public void process() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package org.dromara.common.liteflow.config;
|
||||
|
||||
import org.dromara.common.liteflow.component.*;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
/**
|
||||
* LiteFlow 公共节点自动配置。
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnProperty(value = "liteflow.enable", havingValue = "true")
|
||||
public class LiteFlowAutoConfiguration {
|
||||
|
||||
/**
|
||||
* 空节点,用于显式表达无需处理的分支。
|
||||
*
|
||||
* @return 空节点
|
||||
*/
|
||||
@Bean("noop")
|
||||
public NoopComponent noopComponent() {
|
||||
return new NoopComponent();
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败节点,用于显式抛出业务失败提示。
|
||||
*
|
||||
* @return 失败节点
|
||||
*/
|
||||
@Bean("fail")
|
||||
public FailComponent failComponent() {
|
||||
return new FailComponent();
|
||||
}
|
||||
|
||||
/**
|
||||
* 上下文必填校验节点。
|
||||
*
|
||||
* @return 上下文必填校验节点
|
||||
*/
|
||||
@Bean("contextRequired")
|
||||
public ContextRequiredComponent contextRequiredComponent() {
|
||||
return new ContextRequiredComponent();
|
||||
}
|
||||
|
||||
/**
|
||||
* 恒为 true 的条件节点。
|
||||
*
|
||||
* @return true 条件节点
|
||||
*/
|
||||
@Bean("alwaysTrue")
|
||||
public AlwaysTrueComponent alwaysTrueComponent() {
|
||||
return new AlwaysTrueComponent();
|
||||
}
|
||||
|
||||
/**
|
||||
* 恒为 false 的条件节点。
|
||||
*
|
||||
* @return false 条件节点
|
||||
*/
|
||||
@Bean("alwaysFalse")
|
||||
public AlwaysFalseComponent alwaysFalseComponent() {
|
||||
return new AlwaysFalseComponent();
|
||||
}
|
||||
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package org.dromara.common.liteflow.core;
|
||||
|
||||
/**
|
||||
* LiteFlow 失败提示提供者。
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
public interface FailMessageProvider {
|
||||
|
||||
/**
|
||||
* 获取公共失败节点抛出的业务提示。
|
||||
*
|
||||
* @return 失败提示
|
||||
*/
|
||||
String getFailMessage();
|
||||
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package org.dromara.common.liteflow.utils;
|
||||
|
||||
import com.yomahub.liteflow.core.FlowExecutor;
|
||||
import com.yomahub.liteflow.flow.LiteflowResponse;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
|
||||
/**
|
||||
* LiteFlow 执行工具。
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Slf4j
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class LiteFlowUtils {
|
||||
|
||||
/**
|
||||
* 执行 LiteFlow 链路,并按 contextBean 语义透传业务上下文和失败原因。
|
||||
*
|
||||
* @param chainId 链路标识
|
||||
* @param context 链路上下文
|
||||
*/
|
||||
public static void execute(String chainId, Object context) {
|
||||
if (context == null) {
|
||||
throw new ServiceException("LiteFlow 上下文不能为空");
|
||||
}
|
||||
LiteflowResponse response = SpringUtils.getBean(FlowExecutor.class).execute2Resp(chainId, null, context);
|
||||
if (!response.isSuccess()) {
|
||||
Exception cause = response.getCause();
|
||||
log.error("LiteFlow 链路执行失败 chainId={} requestId={} message={} steps={}",
|
||||
chainId, response.getRequestId(), response.getMessage(), response.getExecuteStepStrWithTime(), cause);
|
||||
if (cause instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
throw new ServiceException(cause != null ? cause.getMessage() : response.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
org.dromara.common.liteflow.config.LiteFlowAutoConfiguration
|
||||
+12
-3
@@ -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;
|
||||
|
||||
/**
|
||||
* 其他参数
|
||||
|
||||
+7
-18
@@ -8,7 +8,6 @@ import com.baomidou.mybatisplus.core.injector.methods.SelectList;
|
||||
import com.baomidou.mybatisplus.core.mapper.Mapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
||||
import com.baomidou.mybatisplus.core.toolkit.ArrayUtils;
|
||||
import com.github.yulichang.adapter.base.tookit.VersionUtils;
|
||||
import com.github.yulichang.base.JoinMapper;
|
||||
import com.github.yulichang.method.*;
|
||||
import com.github.yulichang.toolkit.MPJTableMapperHelper;
|
||||
@@ -113,23 +112,13 @@ public class MPJSqlInjector extends DefaultSqlInjector {
|
||||
*/
|
||||
private List<AbstractMethod> getJoinMethod() {
|
||||
List<AbstractMethod> list = new ArrayList<>();
|
||||
if (VersionUtils.compare(VersionUtils.getVersion(), "3.5.0") >= 0) {
|
||||
list.add(new DeleteJoin(SqlMethod.DELETE_JOIN.getMethod()));
|
||||
list.add(new UpdateJoin(SqlMethod.UPDATE_JOIN.getMethod()));
|
||||
list.add(new UpdateJoinAndNull(SqlMethod.UPDATE_JOIN_AND_NULL.getMethod()));
|
||||
list.add(new SelectJoinCount(SqlMethod.SELECT_JOIN_COUNT.getMethod()));
|
||||
list.add(new SelectJoinOne(SqlMethod.SELECT_JOIN_ONE.getMethod()));
|
||||
list.add(new SelectJoinList(SqlMethod.SELECT_JOIN_LIST.getMethod()));
|
||||
list.add(new SelectJoinPage(SqlMethod.SELECT_JOIN_PAGE.getMethod()));
|
||||
} else {
|
||||
list.add(new DeleteJoin());
|
||||
list.add(new UpdateJoin());
|
||||
list.add(new UpdateJoinAndNull());
|
||||
list.add(new SelectJoinCount());
|
||||
list.add(new SelectJoinOne());
|
||||
list.add(new SelectJoinList());
|
||||
list.add(new SelectJoinPage());
|
||||
}
|
||||
list.add(new DeleteJoin(SqlMethod.DELETE_JOIN.getMethod()));
|
||||
list.add(new UpdateJoin(SqlMethod.UPDATE_JOIN.getMethod()));
|
||||
list.add(new UpdateJoinAndNull(SqlMethod.UPDATE_JOIN_AND_NULL.getMethod()));
|
||||
list.add(new SelectJoinCount(SqlMethod.SELECT_JOIN_COUNT.getMethod()));
|
||||
list.add(new SelectJoinOne(SqlMethod.SELECT_JOIN_ONE.getMethod()));
|
||||
list.add(new SelectJoinList(SqlMethod.SELECT_JOIN_LIST.getMethod()));
|
||||
list.add(new SelectJoinPage(SqlMethod.SELECT_JOIN_PAGE.getMethod()));
|
||||
return list;
|
||||
}
|
||||
|
||||
|
||||
+7
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+24
-2
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+38
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定主表查询字段。
|
||||
*
|
||||
|
||||
+5
@@ -12,6 +12,7 @@ import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.system.api.model.LoginUser;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* MP注入处理器
|
||||
@@ -61,6 +62,9 @@ public class InjectionMetaObjectHandler implements MetaObjectHandler {
|
||||
LocalDateTime date = LocalDateTime.now();
|
||||
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, date);
|
||||
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, date);
|
||||
Date legacyDate = new Date();
|
||||
this.strictInsertFill(metaObject, "createTime", Date.class, legacyDate);
|
||||
this.strictInsertFill(metaObject, "updateTime", Date.class, legacyDate);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new ServiceException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_INTERNAL_ERROR);
|
||||
@@ -86,6 +90,7 @@ public class InjectionMetaObjectHandler implements MetaObjectHandler {
|
||||
baseEntity.setUpdateBy(userId);
|
||||
} else {
|
||||
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
|
||||
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new ServiceException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_INTERNAL_ERROR);
|
||||
|
||||
+5
-3
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -20,14 +20,16 @@ public class PlusPostInitTableInfoHandler implements PostInitTableInfoHandler {
|
||||
*
|
||||
* @param tableInfo 表信息
|
||||
* @param configuration MyBatis 配置
|
||||
* @return 调整后的表信息
|
||||
*/
|
||||
@Override
|
||||
public void postTableInfo(TableInfo tableInfo, Configuration configuration) {
|
||||
public TableInfo postTableInfo(TableInfo tableInfo, Configuration configuration) {
|
||||
String flag = SpringUtils.getProperty("mybatis-plus.enableLogicDelete", "true");
|
||||
// 只有关闭时 统一设置false 为true时mp自动判断不处理
|
||||
if (!Convert.toBool(flag)) {
|
||||
ReflectUtils.setFieldValue(tableInfo, "withLogicDelete", false);
|
||||
}
|
||||
return tableInfo;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-14
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+106
@@ -15,6 +15,14 @@ import software.amazon.awssdk.core.ResponseInputStream;
|
||||
import software.amazon.awssdk.core.async.AsyncRequestBody;
|
||||
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
|
||||
import software.amazon.awssdk.core.async.ResponsePublisher;
|
||||
import software.amazon.awssdk.http.SdkHttpFullRequest;
|
||||
import software.amazon.awssdk.http.SdkHttpMethod;
|
||||
import software.amazon.awssdk.http.auth.aws.signer.AwsV4FamilyHttpSigner;
|
||||
import software.amazon.awssdk.http.auth.aws.signer.AwsV4HttpSigner;
|
||||
import software.amazon.awssdk.http.auth.spi.signer.HttpSigner;
|
||||
import software.amazon.awssdk.http.auth.spi.signer.SignRequest;
|
||||
import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
import software.amazon.awssdk.services.s3.S3AsyncClient;
|
||||
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
|
||||
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
|
||||
@@ -25,6 +33,7 @@ import software.amazon.awssdk.transfer.s3.S3TransferManager;
|
||||
import software.amazon.awssdk.transfer.s3.model.CompletedUpload;
|
||||
import software.amazon.awssdk.transfer.s3.model.DownloadRequest;
|
||||
import software.amazon.awssdk.transfer.s3.progress.TransferListener;
|
||||
import software.amazon.awssdk.utils.http.SdkHttpUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.channels.Channels;
|
||||
@@ -33,8 +42,11 @@ import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.net.URI;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
@@ -716,6 +728,9 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
*/
|
||||
@Override
|
||||
public String bucketPresignGetUrl(String bucket, String key, Duration expiredTime) {
|
||||
if (useBucketBoundDomain(bucket)) {
|
||||
return bucketBoundDomainPresignUrl(SdkHttpMethod.GET, key, expiredTime, Collections.emptyMap());
|
||||
}
|
||||
try {
|
||||
return s3Presigner.presignGetObject(getObjectPresignRequestBuilder -> {
|
||||
getObjectPresignRequestBuilder.signatureDuration(expiredTime)
|
||||
@@ -739,6 +754,9 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
*/
|
||||
@Override
|
||||
public String bucketPresignPutUrl(String bucket, String key, Duration expiredTime, Map<String, String> metadata) {
|
||||
if (useBucketBoundDomain(bucket)) {
|
||||
return bucketBoundDomainPresignUrl(SdkHttpMethod.PUT, key, expiredTime, metadata);
|
||||
}
|
||||
try {
|
||||
return s3Presigner.presignPutObject(putObjectPresignRequestBuilder -> {
|
||||
putObjectPresignRequestBuilder.signatureDuration(expiredTime)
|
||||
@@ -751,6 +769,94 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否使用已绑定默认桶的自定义域名生成无桶名预签名 URL。
|
||||
*
|
||||
* @param bucket 存储桶名称
|
||||
* @return 是否使用自定义域名签名
|
||||
*/
|
||||
private boolean useBucketBoundDomain(String bucket) {
|
||||
return config.domain()
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.isPresent() && config.bucket()
|
||||
.filter(defaultBucket -> Objects.equals(defaultBucket, bucket))
|
||||
.isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用绑定默认桶的自定义域名生成预签名 URL,避免 S3 SDK 自动拼接桶名。
|
||||
*
|
||||
* @param method HTTP 方法
|
||||
* @param key 对象键
|
||||
* @param expiredTime 过期时间
|
||||
* @param metadata 对象元数据
|
||||
* @return 预签名 URL
|
||||
*/
|
||||
private String bucketBoundDomainPresignUrl(SdkHttpMethod method, String key, Duration expiredTime, Map<String, String> metadata) {
|
||||
try {
|
||||
URI domainUri = URI.create(config.getDomainUrl());
|
||||
SdkHttpFullRequest.Builder requestBuilder = SdkHttpFullRequest.builder()
|
||||
.method(method)
|
||||
.uri(domainUri)
|
||||
.encodedPath(bucketBoundDomainPath(domainUri, key));
|
||||
if (metadata != null) {
|
||||
metadata.forEach((metadataKey, metadataValue) -> {
|
||||
if (StringUtils.isNotBlank(metadataKey)) {
|
||||
requestBuilder.putHeader("x-amz-meta-" + metadataKey, String.valueOf(metadataValue));
|
||||
}
|
||||
});
|
||||
}
|
||||
AwsCredentialsIdentity credentials = AwsCredentialsIdentity.create(
|
||||
config.accessKey()
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.orElseThrow(() -> S3StorageException.form("accessKey is not configured.")),
|
||||
config.secretKey()
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.orElseThrow(() -> S3StorageException.form("secretKey is not configured."))
|
||||
);
|
||||
Clock signingClock = Clock.fixed(Instant.now(), ZoneOffset.UTC);
|
||||
return AwsV4HttpSigner.create()
|
||||
.sign(SignRequest.builder(credentials)
|
||||
.request(requestBuilder.build())
|
||||
.putProperty(AwsV4HttpSigner.REGION_NAME, config.region().orElse(Region.US_EAST_1).id())
|
||||
.putProperty(AwsV4FamilyHttpSigner.SERVICE_SIGNING_NAME, "s3")
|
||||
.putProperty(AwsV4FamilyHttpSigner.AUTH_LOCATION, AwsV4FamilyHttpSigner.AuthLocation.QUERY_STRING)
|
||||
.putProperty(AwsV4FamilyHttpSigner.PAYLOAD_SIGNING_ENABLED, false)
|
||||
.putProperty(AwsV4FamilyHttpSigner.EXPIRATION_DURATION, expiredTime)
|
||||
.putProperty(HttpSigner.SIGNING_CLOCK, signingClock)
|
||||
.putProperty(AwsV4FamilyHttpSigner.DOUBLE_URL_ENCODE, false)
|
||||
.putProperty(AwsV4FamilyHttpSigner.NORMALIZE_PATH, false)
|
||||
.build())
|
||||
.request()
|
||||
.getUri()
|
||||
.toString();
|
||||
} catch (Exception e) {
|
||||
throw toStorageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建绑定桶域名下的对象访问路径。
|
||||
*
|
||||
* @param domainUri 自定义域名 URI
|
||||
* @param key 对象键
|
||||
* @return 编码后的访问路径
|
||||
*/
|
||||
private String bucketBoundDomainPath(URI domainUri, String key) {
|
||||
String basePath = Optional.ofNullable(domainUri.getRawPath())
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.filter(path -> !"/".equals(path))
|
||||
.orElse("");
|
||||
String objectPath = SdkHttpUtils.urlEncodeIgnoreSlashes(key);
|
||||
if (!basePath.startsWith("/")) {
|
||||
basePath = "/" + basePath;
|
||||
}
|
||||
if (!basePath.endsWith("/")) {
|
||||
basePath = basePath + "/";
|
||||
}
|
||||
return basePath + objectPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传本地路径文件到默认存储桶。
|
||||
*
|
||||
|
||||
+4
-14
@@ -1,6 +1,5 @@
|
||||
package org.dromara.common.oss.config;
|
||||
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import lombok.Builder;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -196,8 +195,8 @@ public class OssClientConfig implements Config<OssClientConfig, OssClientConfig.
|
||||
public String getDomainUrl() {
|
||||
return domain()
|
||||
// 如果已经配置了自定义域名,则优先使用域名
|
||||
// 检查携带协议头
|
||||
.filter(OssClientConfig::hasHttpHeader)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.map(domain -> BucketUrlUtil.rebuildUrlHeader(useHttps, domain.trim()))
|
||||
// 否则使用站点
|
||||
.orElseGet(this::getEndpointUrl);
|
||||
}
|
||||
@@ -274,7 +273,8 @@ public class OssClientConfig implements Config<OssClientConfig, OssClientConfig.
|
||||
*/
|
||||
private String getAccessBaseUrl() {
|
||||
return domain()
|
||||
.filter(OssClientConfig::hasHttpHeader)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.map(String::trim)
|
||||
.orElseGet(this::getEndpoint);
|
||||
}
|
||||
|
||||
@@ -289,16 +289,6 @@ public class OssClientConfig implements Config<OssClientConfig, OssClientConfig.
|
||||
.orElseThrow(() -> S3StorageException.form("endpoint is not configured."));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 URL 是否包含 HTTP 协议头。
|
||||
*
|
||||
* @param url URL
|
||||
* @return 是否包含 HTTP 协议头
|
||||
*/
|
||||
private static boolean hasHttpHeader(String url) {
|
||||
return HttpUtil.isHttp(url) || HttpUtil.isHttps(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* ACL访问策略配置
|
||||
*/
|
||||
|
||||
+5
@@ -31,4 +31,9 @@ public interface MessageConstants {
|
||||
* 心跳响应标识
|
||||
*/
|
||||
String PONG = "pong";
|
||||
|
||||
/**
|
||||
* 同一 token 的新连接替换旧连接时发送给旧连接的控制消息。
|
||||
*/
|
||||
String KICKED = "kicked";
|
||||
}
|
||||
|
||||
+16
@@ -65,6 +65,7 @@ public class SseEmitterSessionManager implements PushSessionManager {
|
||||
// 关闭已存在的SseEmitter,防止超过最大连接数
|
||||
SseEmitter oldEmitter = emitters.remove(token);
|
||||
if (oldEmitter != null) {
|
||||
sendKickedMessage(oldEmitter);
|
||||
oldEmitter.complete();
|
||||
}
|
||||
|
||||
@@ -103,6 +104,21 @@ public class SseEmitterSessionManager implements PushSessionManager {
|
||||
return emitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知旧连接已被同 token 新连接替换。
|
||||
*
|
||||
* @param emitter 旧 SSE 连接
|
||||
*/
|
||||
private void sendKickedMessage(SseEmitter emitter) {
|
||||
try {
|
||||
emitter.send(SseEmitter.event()
|
||||
.name("message")
|
||||
.data(MessageConstants.KICKED));
|
||||
} catch (Exception ignore) {
|
||||
// 旧连接可能已断开,忽略通知失败
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开指定用户的 SSE 连接
|
||||
*
|
||||
|
||||
+14
@@ -5,6 +5,7 @@ import cn.hutool.core.map.MapUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.utils.ThreadUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.push.constant.MessageConstants;
|
||||
import org.dromara.common.push.dto.PushDTO;
|
||||
import org.dromara.common.push.properties.MessageProperties;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
@@ -61,6 +62,7 @@ public class WebSocketSessionManager implements PushSessionManager {
|
||||
Map<String, WebSocketSession> sessions = USER_TOKEN_SESSIONS.computeIfAbsent(userId, key -> new ConcurrentHashMap<>());
|
||||
// 移除并关闭旧的同token会话,避免重复连接
|
||||
WebSocketSession oldSession = sessions.remove(token);
|
||||
sendKickedMessage(oldSession);
|
||||
closeSession(oldSession, CloseStatus.NORMAL);
|
||||
// 存储新会话
|
||||
sessions.put(token, session);
|
||||
@@ -129,6 +131,18 @@ public class WebSocketSessionManager implements PushSessionManager {
|
||||
toRemoveUsers.forEach(USER_TOKEN_SESSIONS::remove);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知旧连接已被同 token 新连接替换。
|
||||
*
|
||||
* @param session 旧 WebSocket 会话
|
||||
*/
|
||||
private void sendKickedMessage(WebSocketSession session) {
|
||||
if (session == null || !session.isOpen()) {
|
||||
return;
|
||||
}
|
||||
sendMessage(session, MessageConstants.KICKED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅消息通道
|
||||
* 注册消息消费者,监听Redis消息推送
|
||||
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package org.dromara.common.redis.config;
|
||||
|
||||
import cn.hutool.http.HttpStatus;
|
||||
import com.baomidou.lock.LockFailureStrategy;
|
||||
import com.baomidou.lock.spring.boot.autoconfigure.LockAutoConfiguration;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Role;
|
||||
|
||||
/**
|
||||
* Lock4j 配置
|
||||
*
|
||||
* @author AprilWind
|
||||
*/
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
@AutoConfiguration(before = LockAutoConfiguration.class)
|
||||
public class Lock4jConfig {
|
||||
|
||||
/**
|
||||
* Lock4j 获取锁失败策略。
|
||||
*/
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public LockFailureStrategy defaultLockFailureStrategy() {
|
||||
return (key, method, arguments) -> {
|
||||
throw new ServiceException("业务处理中,请稍后再试...", HttpStatus.HTTP_UNAVAILABLE);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
+1
@@ -2,3 +2,4 @@ org.dromara.common.redis.config.RedisConfig
|
||||
org.dromara.common.redis.config.CacheConfig
|
||||
org.dromara.common.redis.config.IdempotentConfig
|
||||
org.dromara.common.redis.config.RateLimiterConfig
|
||||
org.dromara.common.redis.config.Lock4jConfig
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
|
||||
<artifactId>ruoyi-common-satoken</artifactId>
|
||||
|
||||
<description>
|
||||
ruoyi-common-satoken 权限认证
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- 核心模块 -->
|
||||
|
||||
+2
-2
@@ -46,10 +46,10 @@ public class SaTokenExceptionHandler {
|
||||
String requestURI = request.getRequestURI();
|
||||
log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, e.getMessage());
|
||||
String msg = switch (e.getType()) {
|
||||
case NotLoginException.TOKEN_TIMEOUT -> "登录已过期,请重新登录";
|
||||
case NotLoginException.TOKEN_TIMEOUT,
|
||||
NotLoginException.TOKEN_FREEZE -> "登录已过期,请重新登录";
|
||||
case NotLoginException.BE_REPLACED -> "当前账号已在其他设备登录,您已被强制下线";
|
||||
case NotLoginException.KICK_OUT -> "账号已被管理员强制下线";
|
||||
case NotLoginException.TOKEN_FREEZE -> "账号已被冻结,请联系管理员处理";
|
||||
default -> "登录状态异常,请重新登录";
|
||||
};
|
||||
return R.fail(HttpStatus.HTTP_UNAUTHORIZED, msg);
|
||||
|
||||
@@ -18,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>
|
||||
|
||||
-179
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
-138
@@ -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();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -68,7 +68,7 @@ public class SocialLoginConfigProperties {
|
||||
private String clientOsType;
|
||||
|
||||
/**
|
||||
* maxkey 服务器地址
|
||||
* 自托管授权服务器地址
|
||||
*/
|
||||
private String serverUrl;
|
||||
|
||||
|
||||
-119
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
-50
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
-101
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
-52
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
-156
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-51
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+1
-3
@@ -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);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
|
||||
FROM bellsoft/liberica-openjdk-rocky:21.0.11-cds
|
||||
# FROM bellsoft/liberica-openjdk-rocky:25.0.3-cds
|
||||
FROM bellsoft/liberica-openjdk-rocky:21.0.12-cds
|
||||
# FROM bellsoft/liberica-openjdk-rocky:25.0.4-cds
|
||||
# FROM findepi/graalvm:java21-native
|
||||
|
||||
LABEL maintainer="Lion Li"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
|
||||
FROM bellsoft/liberica-openjdk-rocky:21.0.11-cds
|
||||
# FROM bellsoft/liberica-openjdk-rocky:25.0.3-cds
|
||||
FROM bellsoft/liberica-openjdk-rocky:21.0.12-cds
|
||||
# FROM bellsoft/liberica-openjdk-rocky:25.0.4-cds
|
||||
# FROM findepi/graalvm:java21-native
|
||||
|
||||
LABEL maintainer="Lion Li"
|
||||
|
||||
@@ -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>
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ public class SnailAiServerApplication {
|
||||
* @param args 启动参数
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
com.aizuda.snail.ai.starter.SnailAiSpringbootApplication.main(args);
|
||||
com.aizuda.snail.ai.starter.SnailAiApplication.main(args);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -35,6 +35,24 @@ snail-ai:
|
||||
access-key: minioadmin
|
||||
secret-key: minioadmin
|
||||
bucket: snail-ai
|
||||
# RAG 文档解析与图片 OCR 配置
|
||||
rag:
|
||||
docling:
|
||||
enabled: ${SNAIL_AI_RAG_DOCLING_ENABLED:true}
|
||||
url: ${SNAIL_AI_RAG_DOCLING_URL:http://127.0.0.1:5100}
|
||||
timeout-seconds: ${SNAIL_AI_RAG_DOCLING_TIMEOUT_SECONDS:300}
|
||||
concurrency: ${SNAIL_AI_RAG_DOCLING_CONCURRENCY:3}
|
||||
health-timeout-millis: ${SNAIL_AI_RAG_DOCLING_HEALTH_TIMEOUT_MILLIS:3000}
|
||||
poll-interval-millis: ${SNAIL_AI_RAG_DOCLING_POLL_INTERVAL_MILLIS:3000}
|
||||
status-timeout-millis: ${SNAIL_AI_RAG_DOCLING_STATUS_TIMEOUT_MILLIS:10000}
|
||||
result-timeout-millis: ${SNAIL_AI_RAG_DOCLING_RESULT_TIMEOUT_MILLIS:30000}
|
||||
max-image-count: ${SNAIL_AI_RAG_DOCLING_MAX_IMAGE_COUNT:100}
|
||||
max-image-bytes: ${SNAIL_AI_RAG_DOCLING_MAX_IMAGE_BYTES:10485760}
|
||||
paddle-ocr-enabled: ${SNAIL_AI_RAG_DOCLING_PADDLE_OCR_ENABLED:true}
|
||||
paddle-ocr-url: ${SNAIL_AI_RAG_DOCLING_PADDLE_OCR_URL:http://127.0.0.1:8866/ocr}
|
||||
paddle-ocr-timeout-millis: ${SNAIL_AI_RAG_DOCLING_PADDLE_OCR_TIMEOUT_MILLIS:180000}
|
||||
paddle-ocr-batch-size: ${SNAIL_AI_RAG_DOCLING_PADDLE_OCR_BATCH_SIZE:5}
|
||||
vision-ocr-fallback-enabled: ${SNAIL_AI_RAG_DOCLING_VISION_OCR_FALLBACK_ENABLED:false}
|
||||
# 短期记忆配置
|
||||
memory:
|
||||
short-term:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
|
||||
FROM bellsoft/liberica-openjdk-rocky:21.0.11-cds
|
||||
# FROM bellsoft/liberica-openjdk-rocky:25.0.3-cds
|
||||
FROM bellsoft/liberica-openjdk-rocky:21.0.12-cds
|
||||
# FROM bellsoft/liberica-openjdk-rocky:25.0.4-cds
|
||||
# FROM findepi/graalvm:java21-native
|
||||
|
||||
LABEL maintainer="Lion Li"
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.common.web.core.BaseController;
|
||||
import org.dromara.system.api.model.LoginUser;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -27,6 +28,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
@RestController
|
||||
@RequestMapping("/snail-ai")
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(prefix = "snail-ai.open-api", name = "enabled", havingValue = "true")
|
||||
public class SnailAiController extends BaseController {
|
||||
|
||||
/**
|
||||
|
||||
-8
@@ -1,14 +1,11 @@
|
||||
package org.dromara.gen.config.properties;
|
||||
|
||||
import cn.hutool.extra.template.TemplateConfig;
|
||||
import lombok.Data;
|
||||
import org.dromara.common.core.factory.YmlPropertySourceFactory;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 代码生成配置属性
|
||||
*
|
||||
@@ -40,9 +37,4 @@ public class GenProperties {
|
||||
*/
|
||||
private String tablePrefix;
|
||||
|
||||
/**
|
||||
* 模板配置
|
||||
*/
|
||||
private TemplateConfig templateConfig = new TemplateConfig(StandardCharsets.UTF_8, null, TemplateConfig.ResourceMode.CLASSPATH);
|
||||
|
||||
}
|
||||
|
||||
@@ -85,6 +85,11 @@ public interface GenConstants {
|
||||
*/
|
||||
String FRONTEND_TYPE_VUE = "vue";
|
||||
|
||||
/**
|
||||
* React 前端模板类型,对应模板根目录下的 react 目录。
|
||||
*/
|
||||
String FRONTEND_TYPE_REACT = "react";
|
||||
|
||||
/**
|
||||
* 树根节点值
|
||||
*/
|
||||
@@ -103,38 +108,39 @@ public interface GenConstants {
|
||||
/**
|
||||
* 数据库字符串类型
|
||||
*/
|
||||
String[] COLUMNTYPE_STR = {"char", "varchar", "enum", "set", "nchar", "nvarchar", "varchar2", "nvarchar2"};
|
||||
String[] COLUMNTYPE_STR = {"char", "varchar", "enum", "set", "nchar", "nvarchar", "varchar2", "nvarchar2",
|
||||
"long varchar", "long nvarchar", "uniqueidentifier", "uuid", "json", "jsonb", "xml"};
|
||||
|
||||
/**
|
||||
* 数据库文本类型
|
||||
*/
|
||||
String[] COLUMNTYPE_TEXT = {"tinytext", "text", "mediumtext", "longtext", "binary", "varbinary", "blob",
|
||||
"ntext", "image", "bytea"};
|
||||
"ntext", "image", "bytea", "clob", "nclob", "long", "long raw", "raw"};
|
||||
|
||||
/**
|
||||
* 数据库时间类型
|
||||
*/
|
||||
String[] COLUMNTYPE_TIME = {"datetime", "time", "date", "timestamp", "year", "interval",
|
||||
"smalldatetime", "datetime2", "datetimeoffset", "timestamptz"};
|
||||
"smalldatetime", "datetime2", "datetimeoffset", "timestamptz", "timestamp with time zone",
|
||||
"timestamp without time zone", "time with time zone", "time without time zone"};
|
||||
|
||||
/**
|
||||
* 数据库数字类型
|
||||
*/
|
||||
String[] COLUMNTYPE_NUMBER = {"tinyint", "smallint", "mediumint", "int", "int2", "int4", "int8", "number", "integer",
|
||||
"bit", "bigint", "float", "float4", "float8", "double", "decimal", "numeric", "real", "double precision",
|
||||
"smallserial", "serial", "bigserial", "money", "smallmoney"};
|
||||
"bit", "boolean", "bool", "bigint", "float", "float4", "float8", "double", "decimal", "dec", "fixed",
|
||||
"numeric", "real", "double precision", "binary_float", "binary_double", "smallserial", "serial",
|
||||
"serial2", "serial4", "serial8", "bigserial", "money", "smallmoney"};
|
||||
|
||||
/**
|
||||
* BO对象 不需要添加字段
|
||||
*/
|
||||
String[] COLUMNNAME_NOT_ADD = {"create_dept", "create_by", "create_time", "del_flag", "update_by",
|
||||
"update_time", "version"};
|
||||
String[] COLUMNNAME_NOT_ADD = {"del_flag", "version"};
|
||||
|
||||
/**
|
||||
* BO对象 不需要编辑字段
|
||||
*/
|
||||
String[] COLUMNNAME_NOT_EDIT = {"create_dept", "create_by", "create_time", "del_flag", "update_by",
|
||||
"update_time", "version"};
|
||||
String[] COLUMNNAME_NOT_EDIT = {"del_flag", "version"};
|
||||
|
||||
/**
|
||||
* VO对象 不需要返回字段
|
||||
@@ -145,8 +151,7 @@ public interface GenConstants {
|
||||
/**
|
||||
* BO对象 不需要查询字段
|
||||
*/
|
||||
String[] COLUMNNAME_NOT_QUERY = {"id", "create_dept", "create_by", "create_time", "del_flag", "update_by",
|
||||
"update_time", "remark", "version"};
|
||||
String[] COLUMNNAME_NOT_QUERY = {"id", "del_flag", "remark", "version"};
|
||||
|
||||
/**
|
||||
* Entity基类字段
|
||||
@@ -228,6 +233,11 @@ public interface GenConstants {
|
||||
*/
|
||||
String TYPE_DOUBLE = "Double";
|
||||
|
||||
/**
|
||||
* 单精度浮点型
|
||||
*/
|
||||
String TYPE_FLOAT = "Float";
|
||||
|
||||
/**
|
||||
* 高精度计算类型
|
||||
*/
|
||||
|
||||
@@ -156,7 +156,7 @@ public class GenTableColumn extends BaseEntity {
|
||||
}
|
||||
if (StringUtils.equalsAny(this.javaType,
|
||||
GenConstants.TYPE_LONG, GenConstants.TYPE_INTEGER, GenConstants.TYPE_DOUBLE,
|
||||
"Float", GenConstants.TYPE_BIGDECIMAL)) {
|
||||
GenConstants.TYPE_FLOAT, GenConstants.TYPE_BIGDECIMAL)) {
|
||||
return "number";
|
||||
}
|
||||
if (StringUtils.equals(this.javaType, GenConstants.TYPE_BOOLEAN)) {
|
||||
@@ -174,7 +174,7 @@ public class GenTableColumn extends BaseEntity {
|
||||
if (StringUtils.equals(this.javaType, GenConstants.TYPE_BOOLEAN)) {
|
||||
return "true";
|
||||
}
|
||||
if (StringUtils.equalsAny(this.javaType, GenConstants.TYPE_INTEGER, GenConstants.TYPE_LONG)) {
|
||||
if (isNumberType()) {
|
||||
return "0";
|
||||
}
|
||||
return "'0'";
|
||||
@@ -189,12 +189,23 @@ public class GenTableColumn extends BaseEntity {
|
||||
if (StringUtils.equals(this.javaType, GenConstants.TYPE_BOOLEAN)) {
|
||||
return "false";
|
||||
}
|
||||
if (StringUtils.equalsAny(this.javaType, GenConstants.TYPE_INTEGER, GenConstants.TYPE_LONG)) {
|
||||
if (isNumberType()) {
|
||||
return "1";
|
||||
}
|
||||
return "'1'";
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前字段是否为数值类型。
|
||||
*
|
||||
* @return 数值类型返回 {@code true}
|
||||
*/
|
||||
public boolean isNumberType() {
|
||||
return StringUtils.equalsAny(this.javaType,
|
||||
GenConstants.TYPE_INTEGER, GenConstants.TYPE_LONG, GenConstants.TYPE_DOUBLE,
|
||||
GenConstants.TYPE_FLOAT, GenConstants.TYPE_BIGDECIMAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为日期范围查询字段。
|
||||
*
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ public class GenTableServiceImpl implements IGenTableService {
|
||||
private final GenTableMapper tableMapper;
|
||||
private final GenTableColumnMapper genTableColumnMapper;
|
||||
|
||||
private static final String[] TABLE_IGNORE = new String[]{"sj_", "flow_", "gen_"};
|
||||
private static final String[] TABLE_IGNORE = new String[]{"sai_", "sj_", "flow_", "gen_"};
|
||||
|
||||
/**
|
||||
* 查询业务字段列表
|
||||
|
||||
@@ -46,7 +46,7 @@ public class GenUtils {
|
||||
* @param table 所属业务表对象
|
||||
*/
|
||||
public static void initColumnField(GenTableColumn column, GenTable table) {
|
||||
String dataType = getDbType(column.getColumnType()).toLowerCase();
|
||||
String dataType = normalizeDbType(getDbType(column.getColumnType()));
|
||||
// 统一转小写 避免有些数据库默认大写问题 如果需要特别书写方式 请在实体类增加注解标注别名
|
||||
String columnName = column.getColumnName().toLowerCase();
|
||||
Integer columnLength = getColumnLength(column.getColumnType());
|
||||
@@ -58,6 +58,8 @@ public class GenUtils {
|
||||
column.setJavaField(StringUtils.toCamelCase(columnName));
|
||||
// 设置默认类型
|
||||
column.setJavaType(GenConstants.TYPE_STRING);
|
||||
// 设置默认HTML类型
|
||||
column.setHtmlType(GenConstants.HTML_INPUT);
|
||||
column.setQueryType(GenConstants.QUERY_EQ);
|
||||
|
||||
if (arraysContains(GenConstants.COLUMNTYPE_STR, dataType) || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType)) {
|
||||
@@ -148,16 +150,20 @@ public class GenUtils {
|
||||
if (isBooleanColumn(dataType, columnLength, columnScale, columnName)) {
|
||||
return GenConstants.TYPE_BOOLEAN;
|
||||
}
|
||||
if (arraysContains(new String[]{"decimal", "numeric", "money", "smallmoney"}, dataType)) {
|
||||
return columnScale > 0 ? GenConstants.TYPE_BIGDECIMAL : resolveIntegerJavaType(columnLength);
|
||||
if (arraysContains(new String[]{"decimal", "dec", "fixed", "numeric", "money", "smallmoney"}, dataType)) {
|
||||
return GenConstants.TYPE_BIGDECIMAL;
|
||||
}
|
||||
if (arraysContains(new String[]{"float", "float4", "float8", "double", "real", "double precision"}, dataType)) {
|
||||
if (arraysContains(new String[]{"float4", "real", "binary_float"}, dataType)) {
|
||||
return GenConstants.TYPE_FLOAT;
|
||||
}
|
||||
if (arraysContains(new String[]{"float", "float8", "double", "double precision", "binary_double"}, dataType)) {
|
||||
return GenConstants.TYPE_DOUBLE;
|
||||
}
|
||||
if (arraysContains(new String[]{"bigint", "int8", "bigserial"}, dataType)) {
|
||||
if (arraysContains(new String[]{"bigint", "int8", "bigserial", "serial8"}, dataType)) {
|
||||
return GenConstants.TYPE_LONG;
|
||||
}
|
||||
if (arraysContains(new String[]{"smallint", "mediumint", "int", "int2", "int4", "integer", "smallserial", "serial"}, dataType)) {
|
||||
if (arraysContains(new String[]{"tinyint", "smallint", "mediumint", "int", "int2", "int4", "integer",
|
||||
"smallserial", "serial", "serial2", "serial4"}, dataType)) {
|
||||
return GenConstants.TYPE_INTEGER;
|
||||
}
|
||||
if (StringUtils.equals(dataType, "number")) {
|
||||
@@ -169,6 +175,19 @@ public class GenUtils {
|
||||
return GenConstants.TYPE_LONG;
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化数据库字段类型,去除 MySQL 等数据库的类型修饰。
|
||||
*
|
||||
* @param dataType 数据库字段类型
|
||||
* @return 规范化后的数据库字段类型
|
||||
*/
|
||||
private static String normalizeDbType(String dataType) {
|
||||
return StringUtils.trim(dataType)
|
||||
.toLowerCase()
|
||||
.replace(" unsigned", "")
|
||||
.replace(" zerofill", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据整数字段长度解析 Java 类型。
|
||||
*
|
||||
|
||||
+49
-11
@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.lang.Dict;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.extra.template.TemplateConfig;
|
||||
import cn.hutool.extra.template.TemplateEngine;
|
||||
import cn.hutool.extra.template.TemplateUtil;
|
||||
import cn.hutool.extra.template.engine.freemarker.FreemarkerEngine;
|
||||
@@ -12,12 +13,10 @@ import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.DateUtils;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.mybatis.enums.DataBaseType;
|
||||
import org.dromara.common.mybatis.helper.DataBaseHelper;
|
||||
import org.dromara.gen.config.properties.GenProperties;
|
||||
import org.dromara.gen.constant.GenConstants;
|
||||
import org.dromara.gen.domain.GenTable;
|
||||
import org.dromara.gen.domain.GenTableColumn;
|
||||
@@ -27,6 +26,7 @@ import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.core.io.support.ResourcePatternResolver;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -59,9 +59,13 @@ public class TemplateEngineUtils {
|
||||
|
||||
static {
|
||||
// 模板引擎初始化
|
||||
GenProperties properties = SpringUtils.getBean(GenProperties.class);
|
||||
properties.getTemplateConfig().setCustomEngine(FreemarkerEngine.class);
|
||||
TEMPLATE_ENGINE = TemplateUtil.createEngine(properties.getTemplateConfig());
|
||||
TemplateConfig templateConfig = new TemplateConfig(StandardCharsets.UTF_8, StringUtils.EMPTY, TemplateConfig.ResourceMode.CLASSPATH);
|
||||
templateConfig.setCustomEngine(FreemarkerEngine.class);
|
||||
TEMPLATE_ENGINE = TemplateUtil.createEngine(templateConfig);
|
||||
// 禁用数字千分位格式化 避免 Long 类型的 ID 渲染是被加上逗号
|
||||
if (TEMPLATE_ENGINE instanceof FreemarkerEngine freemarkerEngine) {
|
||||
freemarkerEngine.getConfiguration().setNumberFormat("computer");
|
||||
}
|
||||
TEMPLATE_MAPPER = PathNamedTemplate.form(TEMPLATE_ENGINE, GenConstants.TEMPLATE_PATHS);
|
||||
}
|
||||
|
||||
@@ -168,6 +172,7 @@ public class TemplateEngineUtils {
|
||||
boolean needDigit = false;
|
||||
boolean needDateField = false;
|
||||
boolean needSwitchField = false;
|
||||
boolean needParseTime = false;
|
||||
String firstTreeListField = StringUtils.EMPTY;
|
||||
for (GenTableColumn column : genTable.getColumns()) {
|
||||
boolean writable = column.isInsert() || column.isEdit();
|
||||
@@ -183,6 +188,7 @@ public class TemplateEngineUtils {
|
||||
needDigit = needDigit || writable && StringUtils.equals(column.getHtmlType(), GenConstants.HTML_INPUT_NUMBER);
|
||||
needDateField = needDateField || writable && StringUtils.equals(column.getHtmlType(), GenConstants.HTML_DATETIME);
|
||||
needSwitchField = needSwitchField || (writable || column.isList()) && StringUtils.equals(column.getHtmlType(), GenConstants.HTML_SWITCH);
|
||||
needParseTime = needParseTime || column.isList() && StringUtils.equals(column.getHtmlType(), GenConstants.HTML_DATETIME);
|
||||
if (StringUtils.isBlank(firstTreeListField) && column.isList()) {
|
||||
firstTreeListField = column.getJavaField();
|
||||
}
|
||||
@@ -202,6 +208,7 @@ public class TemplateEngineUtils {
|
||||
context.put("needDigit", needDigit);
|
||||
context.put("needDateField", needDateField);
|
||||
context.put("needSwitchField", needSwitchField);
|
||||
context.put("needParseTime", needParseTime);
|
||||
context.put("firstTreeListField", firstTreeListField);
|
||||
}
|
||||
|
||||
@@ -351,6 +358,7 @@ public class TemplateEngineUtils {
|
||||
String javaPath = PROJECT_PATH + "/" + StringUtils.replace(packageName, ".", "/");
|
||||
String mybatisPath = MYBATIS_PATH + "/" + moduleName;
|
||||
String frontendPath = getFrontendPath(genTable.getFrontendType());
|
||||
String frontendPagePath = getFrontendPagePath(genTable.getFrontendType());
|
||||
// templatePath
|
||||
// genFilePathFormat
|
||||
if (template.contains("domain.java.")) {
|
||||
@@ -376,10 +384,10 @@ public class TemplateEngineUtils {
|
||||
} else if (template.contains("types.ts.")) {
|
||||
fileName = StringUtils.format("{}/api/{}/{}/types.ts", frontendPath, moduleName, businessName);
|
||||
} else if (isFrontendPageTemplate(template, GenConstants.FRONTEND_INDEX_TEMPLATE_PREFIX)) {
|
||||
fileName = StringUtils.format("{}/views/{}/{}/index.{}", frontendPath, moduleName, businessName,
|
||||
fileName = StringUtils.format("{}/{}/{}/{}/index.{}", frontendPath, frontendPagePath, moduleName, businessName,
|
||||
getFrontendPageExtension(template, GenConstants.FRONTEND_INDEX_TEMPLATE_PREFIX));
|
||||
} else if (isFrontendPageTemplate(template, GenConstants.FRONTEND_INDEX_TREE_TEMPLATE_PREFIX)) {
|
||||
fileName = StringUtils.format("{}/views/{}/{}/index.{}", frontendPath, moduleName, businessName,
|
||||
fileName = StringUtils.format("{}/{}/{}/{}/index.{}", frontendPath, frontendPagePath, moduleName, businessName,
|
||||
getFrontendPageExtension(template, GenConstants.FRONTEND_INDEX_TREE_TEMPLATE_PREFIX));
|
||||
}
|
||||
return fileName;
|
||||
@@ -395,6 +403,16 @@ public class TemplateEngineUtils {
|
||||
return resolveFrontendType(frontendType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取前端页面输出目录。
|
||||
*
|
||||
* @param frontendType 前端模板类型
|
||||
* @return 页面输出目录
|
||||
*/
|
||||
private static String getFrontendPagePath(String frontendType) {
|
||||
return GenConstants.FRONTEND_TYPE_REACT.equals(resolveFrontendType(frontendType)) ? "pages" : "views";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取前端页面文件扩展名。
|
||||
*
|
||||
@@ -507,16 +525,26 @@ public class TemplateEngineUtils {
|
||||
List<GenTableColumn> columns = genTable.getColumns();
|
||||
HashSet<String> importList = new HashSet<>();
|
||||
for (GenTableColumn column : columns) {
|
||||
if (!column.isSuperColumn() && GenConstants.TYPE_DATE.equals(column.getJavaType())) {
|
||||
if (needImportColumn(column) && GenConstants.TYPE_DATE.equals(column.getJavaType())) {
|
||||
importList.add("java.time.LocalDateTime");
|
||||
importList.add("com.fasterxml.jackson.annotation.JsonFormat");
|
||||
} else if (!column.isSuperColumn() && GenConstants.TYPE_BIGDECIMAL.equals(column.getJavaType())) {
|
||||
} else if (needImportColumn(column) && GenConstants.TYPE_BIGDECIMAL.equals(column.getJavaType())) {
|
||||
importList.add("java.math.BigDecimal");
|
||||
}
|
||||
}
|
||||
return importList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断列是否需要生成类型导入。
|
||||
*
|
||||
* @param column 列对象
|
||||
* @return 需要生成类型导入返回 {@code true}
|
||||
*/
|
||||
private static boolean needImportColumn(GenTableColumn column) {
|
||||
return !column.isSuperColumn() || column.isQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据列类型获取字典组
|
||||
*
|
||||
@@ -700,7 +728,16 @@ public class TemplateEngineUtils {
|
||||
if (StringUtils.equals(column.getJavaType(), GenConstants.TYPE_LONG)) {
|
||||
return value + "L";
|
||||
}
|
||||
if (StringUtils.equalsAny(column.getJavaType(), GenConstants.TYPE_INTEGER, GenConstants.TYPE_DOUBLE, GenConstants.TYPE_BIGDECIMAL)) {
|
||||
if (StringUtils.equals(column.getJavaType(), GenConstants.TYPE_FLOAT)) {
|
||||
return value + "F";
|
||||
}
|
||||
if (StringUtils.equals(column.getJavaType(), GenConstants.TYPE_DOUBLE)) {
|
||||
return value + "D";
|
||||
}
|
||||
if (StringUtils.equals(column.getJavaType(), GenConstants.TYPE_BIGDECIMAL)) {
|
||||
return "new java.math.BigDecimal(\"" + value + "\")";
|
||||
}
|
||||
if (StringUtils.equals(column.getJavaType(), GenConstants.TYPE_INTEGER)) {
|
||||
return value;
|
||||
}
|
||||
return "\"" + value + "\"";
|
||||
@@ -717,7 +754,8 @@ public class TemplateEngineUtils {
|
||||
if (ObjectUtil.isNull(column) || StringUtils.isBlank(value)) {
|
||||
return "undefined";
|
||||
}
|
||||
if (StringUtils.equalsAny(column.getJavaType(), GenConstants.TYPE_LONG, GenConstants.TYPE_INTEGER, GenConstants.TYPE_DOUBLE, GenConstants.TYPE_BIGDECIMAL)) {
|
||||
if (StringUtils.equalsAny(column.getJavaType(), GenConstants.TYPE_LONG, GenConstants.TYPE_INTEGER,
|
||||
GenConstants.TYPE_DOUBLE, GenConstants.TYPE_FLOAT, GenConstants.TYPE_BIGDECIMAL)) {
|
||||
return value;
|
||||
}
|
||||
return "'" + value + "'";
|
||||
|
||||
@@ -30,7 +30,7 @@ public class ${ClassName}Bo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
<#list columns as column>
|
||||
<#if !table.isSuperColumn(column.javaField) && (column.query || column.insert || column.edit)>
|
||||
<#if column.query || (!table.isSuperColumn(column.javaField) && (column.insert || column.edit))>
|
||||
/**
|
||||
* ${column.columnComment}
|
||||
*/
|
||||
@@ -41,7 +41,7 @@ public class ${ClassName}Bo implements Serializable {
|
||||
<#elseif column.edit>
|
||||
<#assign Group = "EditGroup.class">
|
||||
</#if>
|
||||
<#if column.required>
|
||||
<#if column.required && !table.isSuperColumn(column.javaField) && (column.insert || column.edit)>
|
||||
<#if column.javaType == 'String'>
|
||||
@NotBlank(message = "${column.columnComment}不能为空", groups = { ${Group} })
|
||||
<#else>
|
||||
@@ -60,4 +60,3 @@ public class ${ClassName}Bo implements Serializable {
|
||||
</#if>
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+7
-2
@@ -60,11 +60,16 @@ public class ProfileUserVo implements Serializable {
|
||||
*/
|
||||
private String gender;
|
||||
|
||||
/**
|
||||
* 头像 OSS ID
|
||||
*/
|
||||
private Long avatar;
|
||||
|
||||
/**
|
||||
* 头像地址
|
||||
*/
|
||||
@Translation(type = TransConstant.OSS_ID_TO_URL)
|
||||
private Long avatar;
|
||||
@Translation(type = TransConstant.OSS_ID_TO_URL, mapper = "avatar")
|
||||
private String avatarUrl;
|
||||
|
||||
/**
|
||||
* 最后登录IP
|
||||
|
||||
+7
-2
@@ -70,11 +70,16 @@ public class SysUserVo implements Serializable {
|
||||
*/
|
||||
private String gender;
|
||||
|
||||
/**
|
||||
* 头像 OSS ID
|
||||
*/
|
||||
private Long avatar;
|
||||
|
||||
/**
|
||||
* 头像地址
|
||||
*/
|
||||
@Translation(type = TransConstant.OSS_ID_TO_URL)
|
||||
private Long avatar;
|
||||
@Translation(type = TransConstant.OSS_ID_TO_URL, mapper = "avatar")
|
||||
private String avatarUrl;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
|
||||
+3
-3
@@ -219,9 +219,9 @@ public class SysClientServiceImpl implements ISysClientService {
|
||||
* @return 逗号拼接后的规则串
|
||||
*/
|
||||
private String resolveRuleValue(String rawValue, List<String> listValue, UnaryOperator<String> normalizer) {
|
||||
List<String> rules = CollUtil.isNotEmpty(listValue)
|
||||
? listValue
|
||||
: StringUtils.str2List(rawValue, CLIENT_RULE_SEPARATOR_REGEX, true, true);
|
||||
List<String> rules = rawValue != null
|
||||
? StringUtils.str2List(rawValue, CLIENT_RULE_SEPARATOR_REGEX, true, true)
|
||||
: listValue;
|
||||
if (CollUtil.isEmpty(rules)) {
|
||||
return listValue != null || rawValue != null ? "" : null;
|
||||
}
|
||||
|
||||
+3
-7
@@ -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
Reference in New Issue
Block a user