Compare commits
66 Commits
v2026.04(j
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
2c46f1fc09 | |
|
|
0936e04df3 | |
|
|
39f7e9de6a | |
|
|
1ee990464a | |
|
|
116b591eb3 | |
|
|
38f2341d34 | |
|
|
eb7fa0d844 | |
|
|
ed87b45dff | |
|
|
ef63fa3737 | |
|
|
af91987460 | |
|
|
b7fbfc4a19 | |
|
|
cdb4204bf8 | |
|
|
cb5f6750ca | |
|
|
bf35900a25 | |
|
|
c769627aaa | |
|
|
d7cfe9c241 | |
|
|
0c69bedc34 | |
|
|
f5a68ce6f3 | |
|
|
bccf7c79e5 | |
|
|
649362ea33 | |
|
|
10035e1709 | |
|
|
de134cf3b3 | |
|
|
c937f30436 | |
|
|
9431722bae | |
|
|
2ba231aa55 | |
|
|
0c261b4e02 | |
|
|
08c88e0cb3 | |
|
|
0ca9eda251 | |
|
|
1bc987805a | |
|
|
3020c6cdb0 | |
|
|
ffb4e8c158 | |
|
|
ff4ed31c1b | |
|
|
14add8edf0 | |
|
|
6e492e1e6b | |
|
|
7a4c1a0ba4 | |
|
|
9a319b68ff | |
|
|
931472e38d | |
|
|
47660ed156 | |
|
|
fbeec7d731 | |
|
|
8ad9167ddd | |
|
|
1b060dc93f | |
|
|
4363135bf8 | |
|
|
6d213c6746 | |
|
|
b8156e679d | |
|
|
d86fe5e5ca | |
|
|
38ec655d10 | |
|
|
9782e75514 | |
|
|
e7ac652a9c | |
|
|
17943589a4 | |
|
|
d67f91a63e | |
|
|
f6b769fc2d | |
|
|
36fbb9a68b | |
|
|
25a823fe82 | |
|
|
3314376e59 | |
|
|
050edb2db7 | |
|
|
5088b8c2e2 | |
|
|
8637b2a28f | |
|
|
1702fc1acb | |
|
|
d5ab0b06a7 | |
|
|
5cf473d48e | |
|
|
f194e7a3d3 | |
|
|
cf7b6d9cbc | |
|
|
9d1dd25bc7 | |
|
|
63cae8bc31 | |
|
|
b8cff89a12 | |
|
|
3b9ab7fa4a |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 54 KiB |
31
README.md
|
|
@ -31,8 +31,8 @@
|
||||||
| 【完整版】[yudao-cloud](https://gitee.com/zhijiantianya/yudao-cloud) | [`master`](https://gitee.com/zhijiantianya/yudao-cloud/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/zhijiantianya/yudao-cloud/tree/master-jdk17/) 分支 |
|
| 【完整版】[yudao-cloud](https://gitee.com/zhijiantianya/yudao-cloud) | [`master`](https://gitee.com/zhijiantianya/yudao-cloud/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/zhijiantianya/yudao-cloud/tree/master-jdk17/) 分支 |
|
||||||
| 【精简版】[yudao-cloud-mini](https://gitee.com/yudaocode/yudao-cloud-mini) | [`master`](https://gitee.com/yudaocode/yudao-cloud-mini/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/yudaocode/yudao-cloud-mini/tree/master-jdk17/) 分支 |
|
| 【精简版】[yudao-cloud-mini](https://gitee.com/yudaocode/yudao-cloud-mini) | [`master`](https://gitee.com/yudaocode/yudao-cloud-mini/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/yudaocode/yudao-cloud-mini/tree/master-jdk17/) 分支 |
|
||||||
|
|
||||||
* 【完整版】:包括系统功能、基础设施、会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、MES、AI 大模型、IoT 物联网 等功能
|
* 【完整版】:包括系统功能、基础设施、会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、WMS、MES、IM 即时通讯、AI 大模型、IoT 物联网等功能
|
||||||
* 【精简版】:只包括系统功能、基础设施功能,不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、MES、AI 大模型、IoT 物联网 等功能
|
* 【精简版】:只包括系统功能、基础设施功能,不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、WMS、MES、IM 即时通讯、AI 大模型、IoT 物联网等功能
|
||||||
|
|
||||||
可参考 [《迁移文档》](https://cloud.iocoder.cn/migrate-module/) ,只需要 5-10 分钟,即可将【完整版】按需迁移到【精简版】
|
可参考 [《迁移文档》](https://cloud.iocoder.cn/migrate-module/) ,只需要 5-10 分钟,即可将【完整版】按需迁移到【精简版】
|
||||||
|
|
||||||
|
|
@ -105,7 +105,7 @@
|
||||||
|
|
||||||
团队包含专业的项目经理、架构师、前端工程师、后端工程师、测试工程师、运维工程师,可以提供全流程的外包服务。
|
团队包含专业的项目经理、架构师、前端工程师、后端工程师、测试工程师、运维工程师,可以提供全流程的外包服务。
|
||||||
|
|
||||||
项目可以是商城、SCRM 系统、OA 系统、物流系统、ERP 系统、CMS 系统、HIS 系统、支付系统、IM 聊天、微信公众号、微信小程序等等。
|
项目可以是商城、SCRM 系统、OA 系统、物流系统、ERP 系统、CMS 系统、HIS 系统、支付系统、IM 即时通讯、微信公众号、微信小程序等等。
|
||||||
|
|
||||||
## 🐼 内置功能
|
## 🐼 内置功能
|
||||||
|
|
||||||
|
|
@ -115,7 +115,7 @@
|
||||||
|
|
||||||
* 通用模块(必选):系统功能、基础设施
|
* 通用模块(必选):系统功能、基础设施
|
||||||
* 通用模块(可选):工作流程、支付系统、数据报表、会员中心
|
* 通用模块(可选):工作流程、支付系统、数据报表、会员中心
|
||||||
* 业务系统(按需):ERP 系统、CRM 系统、MES 系统、商城系统、微信公众号、AI 大模型、IoT 物联网
|
* 业务系统(按需):Mall 电子商城、OA 办公自动化、ERP 企业资源计划系统、WMS 仓库管理系统、CRM 客户关系管理、CMS 内容管理系统、MES 执行制造系统、AI 大模型平台、IoT 物联网系统、IM 即时通讯系统、Mobile 手机移动端、Report 数据大屏
|
||||||
|
|
||||||
> 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。
|
> 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。
|
||||||
>
|
>
|
||||||
|
|
@ -273,6 +273,14 @@
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
### WMS 系统
|
||||||
|
|
||||||
|
演示地址:<https://cloud.iocoder.cn/wms-preview/>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
### CRM 系统
|
### CRM 系统
|
||||||
|
|
||||||
演示地址:<https://cloud.iocoder.cn/crm-preview/>
|
演示地址:<https://cloud.iocoder.cn/crm-preview/>
|
||||||
|
|
@ -303,6 +311,19 @@
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
### IM 即时通讯
|
||||||
|
|
||||||
|
演示地址(Cloud):<https://cloud.iocoder.cn/im-preview/>
|
||||||
|
|
||||||
|
演示地址(Vue3 + Element Plus):<http://dashboard-vue3.yudao.iocoder.cn>
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
| 聊天界面 | 聊天管理 |
|
||||||
|
| --- | --- |
|
||||||
|
|  |  |
|
||||||
|
|
||||||
## 🐨 技术栈
|
## 🐨 技术栈
|
||||||
|
|
||||||
### 微服务
|
### 微服务
|
||||||
|
|
@ -321,6 +342,8 @@
|
||||||
| `yudao-module-erp` | ERP 系统的 Module 模块 |
|
| `yudao-module-erp` | ERP 系统的 Module 模块 |
|
||||||
| `yudao-module-crm` | CRM 系统的 Module 模块 |
|
| `yudao-module-crm` | CRM 系统的 Module 模块 |
|
||||||
| `yudao-module-mes` | MES 系统的 Module 模块 |
|
| `yudao-module-mes` | MES 系统的 Module 模块 |
|
||||||
|
| `yudao-module-wms` | WMS 系统的 Module 模块 |
|
||||||
|
| `yudao-module-im` | IM 即时通讯的 Module 模块 |
|
||||||
| `yudao-module-ai` | AI 大模型的 Module 模块 |
|
| `yudao-module-ai` | AI 大模型的 Module 模块 |
|
||||||
| `yudao-module-iot` | IoT 物联网的 Module 模块 |
|
| `yudao-module-iot` | IoT 物联网的 Module 模块 |
|
||||||
| `yudao-module-mp` | 微信公众号的 Module 模块 |
|
| `yudao-module-mp` | 微信公众号的 Module 模块 |
|
||||||
|
|
|
||||||
6
pom.xml
|
|
@ -26,6 +26,8 @@
|
||||||
<module>yudao-module-crm</module>
|
<module>yudao-module-crm</module>
|
||||||
<module>yudao-module-iot</module>
|
<module>yudao-module-iot</module>
|
||||||
<module>yudao-module-mes</module>
|
<module>yudao-module-mes</module>
|
||||||
|
<module>yudao-module-wms</module>
|
||||||
|
<module>yudao-module-im</module>
|
||||||
<!-- 友情提示:基于 Spring AI 实现 LLM 大模型的接入,需要使用 JDK17 版本,详细可见 https://doc.iocoder.cn/ai/build/ -->
|
<!-- 友情提示:基于 Spring AI 实现 LLM 大模型的接入,需要使用 JDK17 版本,详细可见 https://doc.iocoder.cn/ai/build/ -->
|
||||||
<!-- <module>yudao-module-ai</module>-->
|
<!-- <module>yudao-module-ai</module>-->
|
||||||
</modules>
|
</modules>
|
||||||
|
|
@ -35,7 +37,7 @@
|
||||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<revision>2026.04-jdk8-SNAPSHOT</revision>
|
<revision>2026.05-jdk8-SNAPSHOT</revision>
|
||||||
<!-- Maven 相关 -->
|
<!-- Maven 相关 -->
|
||||||
<java.version>1.8</java.version>
|
<java.version>1.8</java.version>
|
||||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||||
|
|
@ -44,7 +46,7 @@
|
||||||
<maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version>
|
<maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version>
|
||||||
<flatten-maven-plugin.version>1.7.2</flatten-maven-plugin.version>
|
<flatten-maven-plugin.version>1.7.2</flatten-maven-plugin.version>
|
||||||
<!-- maven-surefire-plugin 暂时无法通过 bom 的依赖读取(兼容老版本 IDEA 2024 及以前版本) -->
|
<!-- maven-surefire-plugin 暂时无法通过 bom 的依赖读取(兼容老版本 IDEA 2024 及以前版本) -->
|
||||||
<lombok.version>1.18.42</lombok.version>
|
<lombok.version>1.18.46</lombok.version>
|
||||||
<spring.boot.version>2.7.18</spring.boot.version>
|
<spring.boot.version>2.7.18</spring.boot.version>
|
||||||
<mapstruct.version>1.6.3</mapstruct.version>
|
<mapstruct.version>1.6.3</mapstruct.version>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
|
|
||||||
|
|
@ -1270,6 +1270,8 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3034, 1, 'ttt', 'tt', 'iot_ota_task_record_status', 0, 'success', '', NULL, '1', '2025-09-06 00:02:21', '1', '2025-09-06 00:02:31', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3034, 1, 'ttt', 'tt', 'iot_ota_task_record_status', 0, 'success', '', NULL, '1', '2025-09-06 00:02:21', '1', '2025-09-06 00:02:31', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3035, 40, '支付宝小程序', '40', 'system_social_type', 0, '', '', '', '1', '2023-11-04 13:05:38', '1', '2023-11-04 13:07:16', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3035, 40, '支付宝小程序', '40', 'system_social_type', 0, '', '', '', '1', '2023-11-04 13:05:38', '1', '2023-11-04 13:07:16', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3036, 60, 'Admin Uniapp 移动端', '60', 'infra_codegen_front_type', 0, '', '', NULL, '1', '2025-12-16 19:25:51', '1', '2025-12-17 09:46:15', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3036, 60, 'Admin Uniapp 移动端', '60', 'infra_codegen_front_type', 0, '', '', NULL, '1', '2025-12-16 19:25:51', '1', '2025-12-17 09:46:15', '0');
|
||||||
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3037, 42, 'Vben5.0 Antdv Next Schema 模版', '42', 'infra_codegen_front_type', 0, '', '', '', '1', NOW(), '1', NOW(), '0');
|
||||||
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3038, 43, 'Vben5.0 Antdv Next 标准模版', '43', 'infra_codegen_front_type', 0, '', '', '', '1', NOW(), '1', NOW(), '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3040, 1, 'UDP', 'udp', 'iot_protocol_type', 0, '', '', 'UDP 协议', '1', '2026-02-04 00:32:47', '1', '2026-02-04 00:32:47', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3040, 1, 'UDP', 'udp', 'iot_protocol_type', 0, '', '', 'UDP 协议', '1', '2026-02-04 00:32:47', '1', '2026-02-04 00:32:47', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3041, 2, 'WebSocket', 'websocket', 'iot_protocol_type', 0, '', '', 'WebSocket 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3041, 2, 'WebSocket', 'websocket', 'iot_protocol_type', 0, '', '', 'WebSocket 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3042, 3, 'HTTP', 'http', 'iot_protocol_type', 0, '', '', 'HTTP 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3042, 3, 'HTTP', 'http', 'iot_protocol_type', 0, '', '', 'HTTP 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', '0');
|
||||||
|
|
@ -1552,9 +1554,9 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3443, 1, '草稿', '0', 'mes_wm_product_produce_status', 0, 'info', '', '草稿状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3443, 1, '草稿', '0', 'mes_wm_product_produce_status', 0, 'info', '', '草稿状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3444, 2, '已完成', '4', 'mes_wm_product_produce_status', 0, 'success', '', '已完成状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3444, 2, '已完成', '4', 'mes_wm_product_produce_status', 0, 'success', '', '已完成状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3445, 3, '已取消', '5', 'mes_wm_product_produce_status', 0, 'danger', '', '已取消状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3445, 3, '已取消', '5', 'mes_wm_product_produce_status', 0, 'danger', '', '已取消状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3446, 0, 'è‰ç¨¿', '0', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3446, 0, '草稿', '0', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3447, 1, '已完æˆ', '4', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3447, 1, '已完成', '4', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3448, 2, '已喿¶ˆ', '5', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3448, 2, '已取消', '5', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||||
COMMIT;
|
COMMIT;
|
||||||
SET IDENTITY_INSERT system_dict_data OFF;
|
SET IDENTITY_INSERT system_dict_data OFF;
|
||||||
-- @formatter:on
|
-- @formatter:on
|
||||||
|
|
@ -5617,4 +5619,3 @@ INSERT INTO yudao_demo03_student (id, name, sex, birthday, description, creator,
|
||||||
COMMIT;
|
COMMIT;
|
||||||
SET IDENTITY_INSERT yudao_demo03_student OFF;
|
SET IDENTITY_INSERT yudao_demo03_student OFF;
|
||||||
-- @formatter:on
|
-- @formatter:on
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,208 @@
|
||||||
|
-- https://github.com/quartz-scheduler/quartz/blob/main/quartz/src/main/resources/org/quartz/impl/jdbcjobstore/tables_postgres.sql
|
||||||
|
-- Thanks to Patrick Lightbody for submitting this...
|
||||||
|
--
|
||||||
|
-- In your Quartz properties file, you'll need to set
|
||||||
|
-- org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
|
||||||
|
DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
|
||||||
|
DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
|
||||||
|
DROP TABLE IF EXISTS QRTZ_LOCKS;
|
||||||
|
DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
|
||||||
|
DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
|
||||||
|
DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS;
|
||||||
|
DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
|
||||||
|
DROP TABLE IF EXISTS QRTZ_TRIGGERS;
|
||||||
|
DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
|
||||||
|
DROP TABLE IF EXISTS QRTZ_CALENDARS;
|
||||||
|
|
||||||
|
CREATE TABLE QRTZ_JOB_DETAILS
|
||||||
|
(
|
||||||
|
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||||
|
JOB_NAME VARCHAR(200) NOT NULL,
|
||||||
|
JOB_GROUP VARCHAR(200) NOT NULL,
|
||||||
|
DESCRIPTION VARCHAR(250) NULL,
|
||||||
|
JOB_CLASS_NAME VARCHAR(250) NOT NULL,
|
||||||
|
IS_DURABLE BOOL NOT NULL,
|
||||||
|
IS_NONCONCURRENT BOOL NOT NULL,
|
||||||
|
IS_UPDATE_DATA BOOL NOT NULL,
|
||||||
|
REQUESTS_RECOVERY BOOL NOT NULL,
|
||||||
|
JOB_DATA BYTEA NULL,
|
||||||
|
PRIMARY KEY (SCHED_NAME, JOB_NAME, JOB_GROUP)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE QRTZ_TRIGGERS
|
||||||
|
(
|
||||||
|
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||||
|
TRIGGER_NAME VARCHAR(200) NOT NULL,
|
||||||
|
TRIGGER_GROUP VARCHAR(200) NOT NULL,
|
||||||
|
JOB_NAME VARCHAR(200) NOT NULL,
|
||||||
|
JOB_GROUP VARCHAR(200) NOT NULL,
|
||||||
|
DESCRIPTION VARCHAR(250) NULL,
|
||||||
|
NEXT_FIRE_TIME BIGINT NULL,
|
||||||
|
PREV_FIRE_TIME BIGINT NULL,
|
||||||
|
PRIORITY INTEGER NULL,
|
||||||
|
TRIGGER_STATE VARCHAR(16) NOT NULL,
|
||||||
|
TRIGGER_TYPE VARCHAR(8) NOT NULL,
|
||||||
|
START_TIME BIGINT NOT NULL,
|
||||||
|
END_TIME BIGINT NULL,
|
||||||
|
CALENDAR_NAME VARCHAR(200) NULL,
|
||||||
|
MISFIRE_INSTR SMALLINT NULL,
|
||||||
|
JOB_DATA BYTEA NULL,
|
||||||
|
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
|
||||||
|
FOREIGN KEY (SCHED_NAME, JOB_NAME, JOB_GROUP)
|
||||||
|
REFERENCES QRTZ_JOB_DETAILS (SCHED_NAME, JOB_NAME, JOB_GROUP)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE QRTZ_SIMPLE_TRIGGERS
|
||||||
|
(
|
||||||
|
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||||
|
TRIGGER_NAME VARCHAR(200) NOT NULL,
|
||||||
|
TRIGGER_GROUP VARCHAR(200) NOT NULL,
|
||||||
|
REPEAT_COUNT BIGINT NOT NULL,
|
||||||
|
REPEAT_INTERVAL BIGINT NOT NULL,
|
||||||
|
TIMES_TRIGGERED BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
|
||||||
|
FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
|
||||||
|
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE QRTZ_CRON_TRIGGERS
|
||||||
|
(
|
||||||
|
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||||
|
TRIGGER_NAME VARCHAR(200) NOT NULL,
|
||||||
|
TRIGGER_GROUP VARCHAR(200) NOT NULL,
|
||||||
|
CRON_EXPRESSION VARCHAR(120) NOT NULL,
|
||||||
|
TIME_ZONE_ID VARCHAR(80),
|
||||||
|
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
|
||||||
|
FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
|
||||||
|
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE QRTZ_SIMPROP_TRIGGERS
|
||||||
|
(
|
||||||
|
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||||
|
TRIGGER_NAME VARCHAR(200) NOT NULL,
|
||||||
|
TRIGGER_GROUP VARCHAR(200) NOT NULL,
|
||||||
|
STR_PROP_1 VARCHAR(512) NULL,
|
||||||
|
STR_PROP_2 VARCHAR(512) NULL,
|
||||||
|
STR_PROP_3 VARCHAR(512) NULL,
|
||||||
|
INT_PROP_1 INT NULL,
|
||||||
|
INT_PROP_2 INT NULL,
|
||||||
|
LONG_PROP_1 BIGINT NULL,
|
||||||
|
LONG_PROP_2 BIGINT NULL,
|
||||||
|
DEC_PROP_1 NUMERIC(13, 4) NULL,
|
||||||
|
DEC_PROP_2 NUMERIC(13, 4) NULL,
|
||||||
|
BOOL_PROP_1 BOOL NULL,
|
||||||
|
BOOL_PROP_2 BOOL NULL,
|
||||||
|
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
|
||||||
|
FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
|
||||||
|
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE QRTZ_BLOB_TRIGGERS
|
||||||
|
(
|
||||||
|
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||||
|
TRIGGER_NAME VARCHAR(200) NOT NULL,
|
||||||
|
TRIGGER_GROUP VARCHAR(200) NOT NULL,
|
||||||
|
BLOB_DATA BYTEA NULL,
|
||||||
|
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
|
||||||
|
FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
|
||||||
|
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE QRTZ_CALENDARS
|
||||||
|
(
|
||||||
|
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||||
|
CALENDAR_NAME VARCHAR(200) NOT NULL,
|
||||||
|
CALENDAR BYTEA NOT NULL,
|
||||||
|
PRIMARY KEY (SCHED_NAME, CALENDAR_NAME)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS
|
||||||
|
(
|
||||||
|
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||||
|
TRIGGER_GROUP VARCHAR(200) NOT NULL,
|
||||||
|
PRIMARY KEY (SCHED_NAME, TRIGGER_GROUP)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE QRTZ_FIRED_TRIGGERS
|
||||||
|
(
|
||||||
|
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||||
|
ENTRY_ID VARCHAR(95) NOT NULL,
|
||||||
|
TRIGGER_NAME VARCHAR(200) NOT NULL,
|
||||||
|
TRIGGER_GROUP VARCHAR(200) NOT NULL,
|
||||||
|
INSTANCE_NAME VARCHAR(200) NOT NULL,
|
||||||
|
FIRED_TIME BIGINT NOT NULL,
|
||||||
|
SCHED_TIME BIGINT NOT NULL,
|
||||||
|
PRIORITY INTEGER NOT NULL,
|
||||||
|
STATE VARCHAR(16) NOT NULL,
|
||||||
|
JOB_NAME VARCHAR(200) NULL,
|
||||||
|
JOB_GROUP VARCHAR(200) NULL,
|
||||||
|
IS_NONCONCURRENT BOOL NULL,
|
||||||
|
REQUESTS_RECOVERY BOOL NULL,
|
||||||
|
PRIMARY KEY (SCHED_NAME, ENTRY_ID)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE QRTZ_SCHEDULER_STATE
|
||||||
|
(
|
||||||
|
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||||
|
INSTANCE_NAME VARCHAR(200) NOT NULL,
|
||||||
|
LAST_CHECKIN_TIME BIGINT NOT NULL,
|
||||||
|
CHECKIN_INTERVAL BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (SCHED_NAME, INSTANCE_NAME)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE QRTZ_LOCKS
|
||||||
|
(
|
||||||
|
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||||
|
LOCK_NAME VARCHAR(40) NOT NULL,
|
||||||
|
PRIMARY KEY (SCHED_NAME, LOCK_NAME)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IDX_QRTZ_J_REQ_RECOVERY
|
||||||
|
ON QRTZ_JOB_DETAILS (SCHED_NAME, REQUESTS_RECOVERY);
|
||||||
|
CREATE INDEX IDX_QRTZ_J_GRP
|
||||||
|
ON QRTZ_JOB_DETAILS (SCHED_NAME, JOB_GROUP);
|
||||||
|
|
||||||
|
CREATE INDEX IDX_QRTZ_T_J
|
||||||
|
ON QRTZ_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP);
|
||||||
|
CREATE INDEX IDX_QRTZ_T_JG
|
||||||
|
ON QRTZ_TRIGGERS (SCHED_NAME, JOB_GROUP);
|
||||||
|
CREATE INDEX IDX_QRTZ_T_C
|
||||||
|
ON QRTZ_TRIGGERS (SCHED_NAME, CALENDAR_NAME);
|
||||||
|
CREATE INDEX IDX_QRTZ_T_G
|
||||||
|
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_GROUP);
|
||||||
|
CREATE INDEX IDX_QRTZ_T_STATE
|
||||||
|
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE);
|
||||||
|
CREATE INDEX IDX_QRTZ_T_N_STATE
|
||||||
|
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP, TRIGGER_STATE);
|
||||||
|
CREATE INDEX IDX_QRTZ_T_N_G_STATE
|
||||||
|
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_GROUP, TRIGGER_STATE);
|
||||||
|
CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME
|
||||||
|
ON QRTZ_TRIGGERS (SCHED_NAME, NEXT_FIRE_TIME);
|
||||||
|
CREATE INDEX IDX_QRTZ_T_NFT_ST
|
||||||
|
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE, NEXT_FIRE_TIME);
|
||||||
|
CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE
|
||||||
|
ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME);
|
||||||
|
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE
|
||||||
|
ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_STATE);
|
||||||
|
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP
|
||||||
|
ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_GROUP, TRIGGER_STATE);
|
||||||
|
|
||||||
|
CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME
|
||||||
|
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME);
|
||||||
|
CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY
|
||||||
|
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME, REQUESTS_RECOVERY);
|
||||||
|
CREATE INDEX IDX_QRTZ_FT_J_G
|
||||||
|
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP);
|
||||||
|
CREATE INDEX IDX_QRTZ_FT_JG
|
||||||
|
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, JOB_GROUP);
|
||||||
|
CREATE INDEX IDX_QRTZ_FT_T_G
|
||||||
|
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP);
|
||||||
|
CREATE INDEX IDX_QRTZ_FT_TG
|
||||||
|
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, TRIGGER_GROUP);
|
||||||
|
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
@ -1371,6 +1371,8 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3034, 1, 'ttt', 'tt', 'iot_ota_task_record_status', 0, 'success', '', NULL, '1', '2025-09-06 00:02:21', '1', '2025-09-06 00:02:31', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3034, 1, 'ttt', 'tt', 'iot_ota_task_record_status', 0, 'success', '', NULL, '1', '2025-09-06 00:02:21', '1', '2025-09-06 00:02:31', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3035, 40, '支付宝小程序', '40', 'system_social_type', 0, '', '', '', '1', '2023-11-04 13:05:38', '1', '2023-11-04 13:07:16', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3035, 40, '支付宝小程序', '40', 'system_social_type', 0, '', '', '', '1', '2023-11-04 13:05:38', '1', '2023-11-04 13:07:16', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3036, 60, 'Admin Uniapp 移动端', '60', 'infra_codegen_front_type', 0, '', '', NULL, '1', '2025-12-16 19:25:51', '1', '2025-12-17 09:46:15', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3036, 60, 'Admin Uniapp 移动端', '60', 'infra_codegen_front_type', 0, '', '', NULL, '1', '2025-12-16 19:25:51', '1', '2025-12-17 09:46:15', '0');
|
||||||
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3037, 42, 'Vben5.0 Antdv Next Schema 模版', '42', 'infra_codegen_front_type', 0, '', '', '', '1', NOW(), '1', NOW(), '0');
|
||||||
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3038, 43, 'Vben5.0 Antdv Next 标准模版', '43', 'infra_codegen_front_type', 0, '', '', '', '1', NOW(), '1', NOW(), '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3040, 1, 'UDP', 'udp', 'iot_protocol_type', 0, '', '', 'UDP 协议', '1', '2026-02-04 00:32:47', '1', '2026-02-04 00:32:47', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3040, 1, 'UDP', 'udp', 'iot_protocol_type', 0, '', '', 'UDP 协议', '1', '2026-02-04 00:32:47', '1', '2026-02-04 00:32:47', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3041, 2, 'WebSocket', 'websocket', 'iot_protocol_type', 0, '', '', 'WebSocket 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3041, 2, 'WebSocket', 'websocket', 'iot_protocol_type', 0, '', '', 'WebSocket 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3042, 3, 'HTTP', 'http', 'iot_protocol_type', 0, '', '', 'HTTP 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3042, 3, 'HTTP', 'http', 'iot_protocol_type', 0, '', '', 'HTTP 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', '0');
|
||||||
|
|
@ -1653,9 +1655,9 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3443, 1, '草稿', '0', 'mes_wm_product_produce_status', 0, 'info', '', '草稿状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3443, 1, '草稿', '0', 'mes_wm_product_produce_status', 0, 'info', '', '草稿状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3444, 2, '已完成', '4', 'mes_wm_product_produce_status', 0, 'success', '', '已完成状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3444, 2, '已完成', '4', 'mes_wm_product_produce_status', 0, 'success', '', '已完成状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3445, 3, '已取消', '5', 'mes_wm_product_produce_status', 0, 'danger', '', '已取消状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3445, 3, '已取消', '5', 'mes_wm_product_produce_status', 0, 'danger', '', '已取消状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3446, 0, 'è‰ç¨¿', '0', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3446, 0, '草稿', '0', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3447, 1, '已完æˆ', '4', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3447, 1, '已完成', '4', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3448, 2, '已喿¶ˆ', '5', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3448, 2, '已取消', '5', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||||
COMMIT;
|
COMMIT;
|
||||||
-- @formatter:on
|
-- @formatter:on
|
||||||
|
|
||||||
|
|
@ -5943,4 +5945,3 @@ COMMIT;
|
||||||
DROP SEQUENCE IF EXISTS yudao_demo03_student_seq;
|
DROP SEQUENCE IF EXISTS yudao_demo03_student_seq;
|
||||||
CREATE SEQUENCE yudao_demo03_student_seq
|
CREATE SEQUENCE yudao_demo03_student_seq
|
||||||
START 10;
|
START 10;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1371,6 +1371,8 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3034, 1, 'ttt', 'tt', 'iot_ota_task_record_status', 0, 'success', '', NULL, '1', '2025-09-06 00:02:21', '1', '2025-09-06 00:02:31', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3034, 1, 'ttt', 'tt', 'iot_ota_task_record_status', 0, 'success', '', NULL, '1', '2025-09-06 00:02:21', '1', '2025-09-06 00:02:31', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3035, 40, '支付宝小程序', '40', 'system_social_type', 0, '', '', '', '1', '2023-11-04 13:05:38', '1', '2023-11-04 13:07:16', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3035, 40, '支付宝小程序', '40', 'system_social_type', 0, '', '', '', '1', '2023-11-04 13:05:38', '1', '2023-11-04 13:07:16', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3036, 60, 'Admin Uniapp 移动端', '60', 'infra_codegen_front_type', 0, '', '', NULL, '1', '2025-12-16 19:25:51', '1', '2025-12-17 09:46:15', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3036, 60, 'Admin Uniapp 移动端', '60', 'infra_codegen_front_type', 0, '', '', NULL, '1', '2025-12-16 19:25:51', '1', '2025-12-17 09:46:15', '0');
|
||||||
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3037, 42, 'Vben5.0 Antdv Next Schema 模版', '42', 'infra_codegen_front_type', 0, '', '', '', '1', NOW(), '1', NOW(), '0');
|
||||||
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3038, 43, 'Vben5.0 Antdv Next 标准模版', '43', 'infra_codegen_front_type', 0, '', '', '', '1', NOW(), '1', NOW(), '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3040, 1, 'UDP', 'udp', 'iot_protocol_type', 0, '', '', 'UDP 协议', '1', '2026-02-04 00:32:47', '1', '2026-02-04 00:32:47', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3040, 1, 'UDP', 'udp', 'iot_protocol_type', 0, '', '', 'UDP 协议', '1', '2026-02-04 00:32:47', '1', '2026-02-04 00:32:47', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3041, 2, 'WebSocket', 'websocket', 'iot_protocol_type', 0, '', '', 'WebSocket 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3041, 2, 'WebSocket', 'websocket', 'iot_protocol_type', 0, '', '', 'WebSocket 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3042, 3, 'HTTP', 'http', 'iot_protocol_type', 0, '', '', 'HTTP 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3042, 3, 'HTTP', 'http', 'iot_protocol_type', 0, '', '', 'HTTP 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', '0');
|
||||||
|
|
@ -1653,9 +1655,9 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3443, 1, '草稿', '0', 'mes_wm_product_produce_status', 0, 'info', '', '草稿状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3443, 1, '草稿', '0', 'mes_wm_product_produce_status', 0, 'info', '', '草稿状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3444, 2, '已完成', '4', 'mes_wm_product_produce_status', 0, 'success', '', '已完成状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3444, 2, '已完成', '4', 'mes_wm_product_produce_status', 0, 'success', '', '已完成状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3445, 3, '已取消', '5', 'mes_wm_product_produce_status', 0, 'danger', '', '已取消状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3445, 3, '已取消', '5', 'mes_wm_product_produce_status', 0, 'danger', '', '已取消状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3446, 0, 'è‰ç¨¿', '0', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3446, 0, '草稿', '0', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3447, 1, '已完æˆ', '4', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3447, 1, '已完成', '4', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3448, 2, '已喿¶ˆ', '5', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3448, 2, '已取消', '5', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||||
COMMIT;
|
COMMIT;
|
||||||
-- @formatter:on
|
-- @formatter:on
|
||||||
|
|
||||||
|
|
@ -5943,4 +5945,3 @@ COMMIT;
|
||||||
DROP SEQUENCE IF EXISTS yudao_demo03_student_seq;
|
DROP SEQUENCE IF EXISTS yudao_demo03_student_seq;
|
||||||
CREATE SEQUENCE yudao_demo03_student_seq
|
CREATE SEQUENCE yudao_demo03_student_seq
|
||||||
START 10;
|
START 10;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1323,6 +1323,8 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3034, 1, 'ttt', 'tt', 'iot_ota_task_record_status', 0, 'success', '', NULL, '1', to_date('2025-09-06 00:02:21', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2025-09-06 00:02:31', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3034, 1, 'ttt', 'tt', 'iot_ota_task_record_status', 0, 'success', '', NULL, '1', to_date('2025-09-06 00:02:21', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2025-09-06 00:02:31', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3035, 40, '支付宝小程序', '40', 'system_social_type', 0, '', '', '', '1', to_date('2023-11-04 13:05:38', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2023-11-04 13:07:16', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3035, 40, '支付宝小程序', '40', 'system_social_type', 0, '', '', '', '1', to_date('2023-11-04 13:05:38', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2023-11-04 13:07:16', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3036, 60, 'Admin Uniapp 移动端', '60', 'infra_codegen_front_type', 0, '', '', NULL, '1', to_date('2025-12-16 19:25:51', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2025-12-17 09:46:15', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3036, 60, 'Admin Uniapp 移动端', '60', 'infra_codegen_front_type', 0, '', '', NULL, '1', to_date('2025-12-16 19:25:51', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2025-12-17 09:46:15', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||||
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3037, 42, 'Vben5.0 Antdv Next Schema 模版', '42', 'infra_codegen_front_type', 0, '', '', '', '1', SYSDATE, '1', SYSDATE, '0');
|
||||||
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3038, 43, 'Vben5.0 Antdv Next 标准模版', '43', 'infra_codegen_front_type', 0, '', '', '', '1', SYSDATE, '1', SYSDATE, '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3040, 1, 'UDP', 'udp', 'iot_protocol_type', 0, '', '', 'UDP 协议', '1', to_date('2026-02-04 00:32:47', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-02-04 00:32:47', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3040, 1, 'UDP', 'udp', 'iot_protocol_type', 0, '', '', 'UDP 协议', '1', to_date('2026-02-04 00:32:47', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-02-04 00:32:47', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3041, 2, 'WebSocket', 'websocket', 'iot_protocol_type', 0, '', '', 'WebSocket 协议', '1', to_date('2026-02-04 00:32:55', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-02-04 00:32:55', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3041, 2, 'WebSocket', 'websocket', 'iot_protocol_type', 0, '', '', 'WebSocket 协议', '1', to_date('2026-02-04 00:32:55', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-02-04 00:32:55', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3042, 3, 'HTTP', 'http', 'iot_protocol_type', 0, '', '', 'HTTP 协议', '1', to_date('2026-02-04 00:32:55', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-02-04 00:32:55', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3042, 3, 'HTTP', 'http', 'iot_protocol_type', 0, '', '', 'HTTP 协议', '1', to_date('2026-02-04 00:32:55', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-02-04 00:32:55', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||||
|
|
@ -1605,9 +1607,9 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3443, 1, '草稿', '0', 'mes_wm_product_produce_status', 0, 'info', '', '草稿状态', '1', to_date('2026-04-05 15:53:46', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-04-05 15:53:46', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3443, 1, '草稿', '0', 'mes_wm_product_produce_status', 0, 'info', '', '草稿状态', '1', to_date('2026-04-05 15:53:46', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-04-05 15:53:46', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3444, 2, '已完成', '4', 'mes_wm_product_produce_status', 0, 'success', '', '已完成状态', '1', to_date('2026-04-05 15:53:46', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-04-05 15:53:46', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3444, 2, '已完成', '4', 'mes_wm_product_produce_status', 0, 'success', '', '已完成状态', '1', to_date('2026-04-05 15:53:46', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-04-05 15:53:46', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3445, 3, '已取消', '5', 'mes_wm_product_produce_status', 0, 'danger', '', '已取消状态', '1', to_date('2026-04-05 15:53:46', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-04-05 15:53:46', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3445, 3, '已取消', '5', 'mes_wm_product_produce_status', 0, 'danger', '', '已取消状态', '1', to_date('2026-04-05 15:53:46', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-04-05 15:53:46', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3446, 0, 'è‰ç¨¿', '0', 'mes_pro_task_status', 0, '', '', NULL, '1', to_date('2026-04-16 09:47:00', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-04-16 09:47:00', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3446, 0, '草稿', '0', 'mes_pro_task_status', 0, '', '', NULL, '1', to_date('2026-04-16 09:47:00', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-04-16 09:47:00', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3447, 1, '已完æˆ', '4', 'mes_pro_task_status', 0, '', '', NULL, '1', to_date('2026-04-16 09:47:00', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-04-16 09:47:00', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3447, 1, '已完成', '4', 'mes_pro_task_status', 0, '', '', NULL, '1', to_date('2026-04-16 09:47:00', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-04-16 09:47:00', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3448, 2, '已喿¶ˆ', '5', 'mes_pro_task_status', 0, '', '', NULL, '1', to_date('2026-04-16 09:47:00', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-04-16 09:47:00', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3448, 2, '已取消', '5', 'mes_pro_task_status', 0, '', '', NULL, '1', to_date('2026-04-16 09:47:00', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-04-16 09:47:00', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||||
COMMIT;
|
COMMIT;
|
||||||
-- @formatter:on
|
-- @formatter:on
|
||||||
|
|
||||||
|
|
@ -5801,4 +5803,3 @@ COMMIT;
|
||||||
|
|
||||||
CREATE SEQUENCE yudao_demo03_student_seq
|
CREATE SEQUENCE yudao_demo03_student_seq
|
||||||
START WITH 10;
|
START WITH 10;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1371,6 +1371,8 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3034, 1, 'ttt', 'tt', 'iot_ota_task_record_status', 0, 'success', '', NULL, '1', '2025-09-06 00:02:21', '1', '2025-09-06 00:02:31', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3034, 1, 'ttt', 'tt', 'iot_ota_task_record_status', 0, 'success', '', NULL, '1', '2025-09-06 00:02:21', '1', '2025-09-06 00:02:31', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3035, 40, '支付宝小程序', '40', 'system_social_type', 0, '', '', '', '1', '2023-11-04 13:05:38', '1', '2023-11-04 13:07:16', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3035, 40, '支付宝小程序', '40', 'system_social_type', 0, '', '', '', '1', '2023-11-04 13:05:38', '1', '2023-11-04 13:07:16', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3036, 60, 'Admin Uniapp 移动端', '60', 'infra_codegen_front_type', 0, '', '', NULL, '1', '2025-12-16 19:25:51', '1', '2025-12-17 09:46:15', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3036, 60, 'Admin Uniapp 移动端', '60', 'infra_codegen_front_type', 0, '', '', NULL, '1', '2025-12-16 19:25:51', '1', '2025-12-17 09:46:15', '0');
|
||||||
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3037, 42, 'Vben5.0 Antdv Next Schema 模版', '42', 'infra_codegen_front_type', 0, '', '', '', '1', NOW(), '1', NOW(), '0');
|
||||||
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3038, 43, 'Vben5.0 Antdv Next 标准模版', '43', 'infra_codegen_front_type', 0, '', '', '', '1', NOW(), '1', NOW(), '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3040, 1, 'UDP', 'udp', 'iot_protocol_type', 0, '', '', 'UDP 协议', '1', '2026-02-04 00:32:47', '1', '2026-02-04 00:32:47', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3040, 1, 'UDP', 'udp', 'iot_protocol_type', 0, '', '', 'UDP 协议', '1', '2026-02-04 00:32:47', '1', '2026-02-04 00:32:47', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3041, 2, 'WebSocket', 'websocket', 'iot_protocol_type', 0, '', '', 'WebSocket 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3041, 2, 'WebSocket', 'websocket', 'iot_protocol_type', 0, '', '', 'WebSocket 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3042, 3, 'HTTP', 'http', 'iot_protocol_type', 0, '', '', 'HTTP 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3042, 3, 'HTTP', 'http', 'iot_protocol_type', 0, '', '', 'HTTP 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', '0');
|
||||||
|
|
@ -1653,9 +1655,9 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3443, 1, '草稿', '0', 'mes_wm_product_produce_status', 0, 'info', '', '草稿状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3443, 1, '草稿', '0', 'mes_wm_product_produce_status', 0, 'info', '', '草稿状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3444, 2, '已完成', '4', 'mes_wm_product_produce_status', 0, 'success', '', '已完成状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3444, 2, '已完成', '4', 'mes_wm_product_produce_status', 0, 'success', '', '已完成状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3445, 3, '已取消', '5', 'mes_wm_product_produce_status', 0, 'danger', '', '已取消状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3445, 3, '已取消', '5', 'mes_wm_product_produce_status', 0, 'danger', '', '已取消状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3446, 0, 'è‰ç¨¿', '0', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3446, 0, '草稿', '0', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3447, 1, '已完æˆ', '4', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3447, 1, '已完成', '4', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3448, 2, '已喿¶ˆ', '5', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3448, 2, '已取消', '5', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||||
COMMIT;
|
COMMIT;
|
||||||
-- @formatter:on
|
-- @formatter:on
|
||||||
|
|
||||||
|
|
@ -5943,4 +5945,3 @@ COMMIT;
|
||||||
DROP SEQUENCE IF EXISTS yudao_demo03_student_seq;
|
DROP SEQUENCE IF EXISTS yudao_demo03_student_seq;
|
||||||
CREATE SEQUENCE yudao_demo03_student_seq
|
CREATE SEQUENCE yudao_demo03_student_seq
|
||||||
START 10;
|
START 10;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3367,6 +3367,10 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
|
||||||
GO
|
GO
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3036, 60, N'Admin Uniapp 移动端', N'60', N'infra_codegen_front_type', 0, N'', N'', NULL, N'1', N'2025-12-16 19:25:51', N'1', N'2025-12-17 09:46:15', N'0')
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3036, 60, N'Admin Uniapp 移动端', N'60', N'infra_codegen_front_type', 0, N'', N'', NULL, N'1', N'2025-12-16 19:25:51', N'1', N'2025-12-17 09:46:15', N'0')
|
||||||
GO
|
GO
|
||||||
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3037, 42, N'Vben5.0 Antdv Next Schema 模版', N'42', N'infra_codegen_front_type', 0, N'', N'', N'', N'1', N'2026-05-16 00:00:00', N'1', N'2026-05-16 00:00:00', N'0')
|
||||||
|
GO
|
||||||
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3038, 43, N'Vben5.0 Antdv Next 标准模版', N'43', N'infra_codegen_front_type', 0, N'', N'', N'', N'1', N'2026-05-16 00:00:00', N'1', N'2026-05-16 00:00:00', N'0')
|
||||||
|
GO
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3040, 1, N'UDP', N'udp', N'iot_protocol_type', 0, N'', N'', N'UDP 协议', N'1', N'2026-02-04 00:32:47', N'1', N'2026-02-04 00:32:47', N'0')
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3040, 1, N'UDP', N'udp', N'iot_protocol_type', 0, N'', N'', N'UDP 协议', N'1', N'2026-02-04 00:32:47', N'1', N'2026-02-04 00:32:47', N'0')
|
||||||
GO
|
GO
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3041, 2, N'WebSocket', N'websocket', N'iot_protocol_type', 0, N'', N'', N'WebSocket 协议', N'1', N'2026-02-04 00:32:55', N'1', N'2026-02-04 00:32:55', N'0')
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3041, 2, N'WebSocket', N'websocket', N'iot_protocol_type', 0, N'', N'', N'WebSocket 协议', N'1', N'2026-02-04 00:32:55', N'1', N'2026-02-04 00:32:55', N'0')
|
||||||
|
|
@ -3931,11 +3935,11 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
|
||||||
GO
|
GO
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3445, 3, N'已取消', N'5', N'mes_wm_product_produce_status', 0, N'danger', N'', N'已取消状态', N'1', N'2026-04-05 15:53:46', N'1', N'2026-04-05 15:53:46', N'0')
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3445, 3, N'已取消', N'5', N'mes_wm_product_produce_status', 0, N'danger', N'', N'已取消状态', N'1', N'2026-04-05 15:53:46', N'1', N'2026-04-05 15:53:46', N'0')
|
||||||
GO
|
GO
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3446, 0, N'è‰ç¨¿', N'0', N'mes_pro_task_status', 0, N'', N'', NULL, N'1', N'2026-04-16 09:47:00', N'1', N'2026-04-16 09:47:00', N'0')
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3446, 0, N'草稿', N'0', N'mes_pro_task_status', 0, N'', N'', NULL, N'1', N'2026-04-16 09:47:00', N'1', N'2026-04-16 09:47:00', N'0')
|
||||||
GO
|
GO
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3447, 1, N'已完æˆ', N'4', N'mes_pro_task_status', 0, N'', N'', NULL, N'1', N'2026-04-16 09:47:00', N'1', N'2026-04-16 09:47:00', N'0')
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3447, 1, N'已完成', N'4', N'mes_pro_task_status', 0, N'', N'', NULL, N'1', N'2026-04-16 09:47:00', N'1', N'2026-04-16 09:47:00', N'0')
|
||||||
GO
|
GO
|
||||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3448, 2, N'已喿¶ˆ', N'5', N'mes_pro_task_status', 0, N'', N'', NULL, N'1', N'2026-04-16 09:47:00', N'1', N'2026-04-16 09:47:00', N'0')
|
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3448, 2, N'已取消', N'5', N'mes_pro_task_status', 0, N'', N'', NULL, N'1', N'2026-04-16 09:47:00', N'1', N'2026-04-16 09:47:00', N'0')
|
||||||
GO
|
GO
|
||||||
SET IDENTITY_INSERT system_dict_data OFF
|
SET IDENTITY_INSERT system_dict_data OFF
|
||||||
GO
|
GO
|
||||||
|
|
@ -13915,4 +13919,3 @@ GO
|
||||||
COMMIT
|
COMMIT
|
||||||
GO
|
GO
|
||||||
-- @formatter:on
|
-- @formatter:on
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,25 @@ docker compose up -d opengauss
|
||||||
docker compose exec opengauss bash -c '/usr/local/opengauss/bin/gsql -U $GS_USERNAME -W $GS_PASSWORD -d postgres -f /tmp/schema.sql'
|
docker compose exec opengauss bash -c '/usr/local/opengauss/bin/gsql -U $GS_USERNAME -W $GS_PASSWORD -d postgres -f /tmp/schema.sql'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 1.8 HighGo 瀚高数据库
|
||||||
|
|
||||||
|
① 下载瀚高官方 Docker 镜像,并加载镜像文件。加载后,将镜像打成本地标签:
|
||||||
|
|
||||||
|
```Bash
|
||||||
|
docker load -i <highgo-image>.tar
|
||||||
|
docker tag <image>:<tag> highgo:local
|
||||||
|
```
|
||||||
|
|
||||||
|
② 在项目 `sql/tools` 目录下运行:
|
||||||
|
|
||||||
|
```Bash
|
||||||
|
docker compose up -d highgo
|
||||||
|
```
|
||||||
|
|
||||||
|
> 注意:不同瀚高镜像的数据目录可能不同,如果容器无法启动,请按镜像实际 `PGDATA` 修改 `docker-compose.yaml` 中的 `highgo` 数据卷挂载目录。
|
||||||
|
|
||||||
|
③ 启动完成后,需要手动导入 Quartz 和项目 SQL。瀚高兼容 PostgreSQL,具体客户端命令以当前镜像为准,可使用 `psql` 或瀚高镜像内置的兼容客户端执行 `/tmp/quartz.sql`、`/tmp/schema.sql`。
|
||||||
|
|
||||||
## 1.X 容器的销毁重建
|
## 1.X 容器的销毁重建
|
||||||
|
|
||||||
开发测试过程中,有时候需要创建全新干净的数据库。由于测试数据 Docker 容器采用数据卷 Volume 挂载数据库实例的数据目录,因此销毁数据需要停止容器后,删除数据卷,然后再重新创建容器。
|
开发测试过程中,有时候需要创建全新干净的数据库。由于测试数据 Docker 容器采用数据卷 Volume 挂载数据库实例的数据目录,因此销毁数据需要停止容器后,删除数据卷,然后再重新创建容器。
|
||||||
|
|
@ -103,7 +122,7 @@ docker volume rm ruoyi-vue-pro_postgres
|
||||||
|
|
||||||
## 2. MySQL 转换其它数据库
|
## 2. MySQL 转换其它数据库
|
||||||
|
|
||||||
项目提供了 `sql/tools/convertor.py` 脚本,支持将 MySQL 转换为 Oracle、PostgreSQL、SQL Server、达梦、人大金仓、OpenGauss 等数据库的脚本。
|
项目提供了 `sql/tools/convertor.py` 脚本,支持将 MySQL 转换为 Oracle、PostgreSQL、SQL Server、达梦、人大金仓、OpenGauss、瀚高等数据库的脚本。
|
||||||
|
|
||||||
### 2.1 实现原理
|
### 2.1 实现原理
|
||||||
|
|
||||||
|
|
@ -118,11 +137,12 @@ pip install simple-ddl-parser
|
||||||
# pip3 install simple-ddl-parser
|
# pip3 install simple-ddl-parser
|
||||||
```
|
```
|
||||||
|
|
||||||
② 在 `sql/tools/` 目录下,执行如下命令打印生成 postgres 的脚本内容,其他可选参数有:`oracle`、`sqlserver`、`dm8`、`kingbase`、`opengauss`:
|
② 在 `sql/tools/` 目录下,执行如下命令打印生成 postgres 的脚本内容,其他可选参数有:`oracle`、`sqlserver`、`dm8`、`kingbase`、`opengauss`、`highgo`:
|
||||||
|
|
||||||
```Bash
|
```Bash
|
||||||
python3 convertor.py postgres
|
python3 convertor.py postgres
|
||||||
# python3 convertor.py postgres > tmp.sql
|
# python3 convertor.py postgres > tmp.sql
|
||||||
|
# python3 convertor.py highgo ../mysql/ruoyi-vue-pro.sql > ../highgo/ruoyi-vue-pro.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
程序将 SQL 脚本打印到终端,可以重定向到临时文件 `tmp.sql`。
|
程序将 SQL 脚本打印到终端,可以重定向到临时文件 `tmp.sql`。
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ uv run --with simple-ddl-parser convertor.py postgres ../mysql/ruoyi-vue-pro.sql
|
||||||
uv run --with simple-ddl-parser convertor.py sqlserver ../mysql/ruoyi-vue-pro.sql > ../sqlserver/ruoyi-vue-pro.sql
|
uv run --with simple-ddl-parser convertor.py sqlserver ../mysql/ruoyi-vue-pro.sql > ../sqlserver/ruoyi-vue-pro.sql
|
||||||
uv run --with simple-ddl-parser convertor.py kingbase ../mysql/ruoyi-vue-pro.sql > ../kingbase/ruoyi-vue-pro.sql
|
uv run --with simple-ddl-parser convertor.py kingbase ../mysql/ruoyi-vue-pro.sql > ../kingbase/ruoyi-vue-pro.sql
|
||||||
uv run --with simple-ddl-parser convertor.py opengauss ../mysql/ruoyi-vue-pro.sql > ../opengauss/ruoyi-vue-pro.sql
|
uv run --with simple-ddl-parser convertor.py opengauss ../mysql/ruoyi-vue-pro.sql > ../opengauss/ruoyi-vue-pro.sql
|
||||||
|
uv run --with simple-ddl-parser convertor.py highgo ../mysql/ruoyi-vue-pro.sql > ../highgo/ruoyi-vue-pro.sql
|
||||||
uv run --with simple-ddl-parser convertor.py oracle ../mysql/ruoyi-vue-pro.sql > ../oracle/ruoyi-vue-pro.sql
|
uv run --with simple-ddl-parser convertor.py oracle ../mysql/ruoyi-vue-pro.sql > ../oracle/ruoyi-vue-pro.sql
|
||||||
uv run --with simple-ddl-parser convertor.py dm8 ../mysql/ruoyi-vue-pro.sql > ../dm/ruoyi-vue-pro-dm8.sql
|
uv run --with simple-ddl-parser convertor.py dm8 ../mysql/ruoyi-vue-pro.sql > ../dm/ruoyi-vue-pro-dm8.sql
|
||||||
"""
|
"""
|
||||||
|
|
@ -77,6 +78,9 @@ def load_and_clean(sql_file: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
class Convertor(ABC):
|
class Convertor(ABC):
|
||||||
|
# 不同数据库的关键字不完全一致;子类按需声明需要转义的列名。
|
||||||
|
reserved_column_names = set()
|
||||||
|
|
||||||
def __init__(self, src: str, db_type) -> None:
|
def __init__(self, src: str, db_type) -> None:
|
||||||
self.src = src
|
self.src = src
|
||||||
self.db_type = db_type
|
self.db_type = db_type
|
||||||
|
|
@ -179,6 +183,31 @@ class Convertor(ABC):
|
||||||
"""
|
"""
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
def escape_column_name(self, name: str) -> str:
|
||||||
|
"""转义目标库保留字列名,例如 Oracle / Kingbase 的 level。"""
|
||||||
|
|
||||||
|
column_name = name.lower()
|
||||||
|
if column_name in self.reserved_column_names:
|
||||||
|
return f'"{column_name}"'
|
||||||
|
return column_name
|
||||||
|
|
||||||
|
def escape_insert_columns(self, insert_script: str) -> str:
|
||||||
|
"""INSERT 显式列清单需要和 CREATE / COMMENT 使用同一套列名转义。"""
|
||||||
|
|
||||||
|
match = re.match(
|
||||||
|
r"(INSERT INTO\s+\S+\s*\()([^)]+)(\)\s+VALUES\s+[\s\S]*)",
|
||||||
|
insert_script,
|
||||||
|
flags=re.IGNORECASE,
|
||||||
|
)
|
||||||
|
if not match:
|
||||||
|
return insert_script
|
||||||
|
|
||||||
|
columns = [
|
||||||
|
self.escape_column_name(column.strip())
|
||||||
|
for column in match.group(2).split(",")
|
||||||
|
]
|
||||||
|
return f"{match.group(1)}{', '.join(columns)}{match.group(3)}"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def inserts(table_name: str, script_content: str) -> Generator:
|
def inserts(table_name: str, script_content: str) -> Generator:
|
||||||
PREFIX = f"INSERT INTO `{table_name}`"
|
PREFIX = f"INSERT INTO `{table_name}`"
|
||||||
|
|
@ -204,18 +233,55 @@ class Convertor(ABC):
|
||||||
Generator[str]: create index 语句
|
Generator[str]: create index 语句
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def generate_columns(columns):
|
for no, index in enumerate(ddl.get("index", []), 1):
|
||||||
keys = [
|
columns = ", ".join(Convertor.index_columns(index.get("columns", [])))
|
||||||
f"{col['name'].lower()}{' ' + col['order'].lower() if col['order'] != 'ASC' else ''}"
|
if not columns:
|
||||||
for col in columns[0]
|
continue
|
||||||
]
|
|
||||||
return ", ".join(keys)
|
|
||||||
|
|
||||||
for no, index in enumerate(ddl["index"], 1):
|
|
||||||
columns = generate_columns(index["columns"])
|
|
||||||
table_name = ddl["table_name"].lower()
|
table_name = ddl["table_name"].lower()
|
||||||
yield f"CREATE INDEX idx_{table_name}_{no:02d} ON {table_name} ({columns})"
|
yield f"CREATE INDEX idx_{table_name}_{no:02d} ON {table_name} ({columns})"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def index_columns(columns) -> list:
|
||||||
|
"""兼容 simple-ddl-parser 不同版本的索引列结构。"""
|
||||||
|
|
||||||
|
keys = []
|
||||||
|
|
||||||
|
def append(name, order="ASC"):
|
||||||
|
if not name:
|
||||||
|
return
|
||||||
|
column_name = str(name).strip("`").lower()
|
||||||
|
column_order = str(order or "ASC").upper()
|
||||||
|
if column_order == "DESC":
|
||||||
|
keys.append(f"{column_name} desc")
|
||||||
|
else:
|
||||||
|
keys.append(column_name)
|
||||||
|
|
||||||
|
def visit(value):
|
||||||
|
# 普通索引常见结构:[[{'name': 'user_id', 'order': 'ASC'}]]
|
||||||
|
if isinstance(value, (list, tuple)):
|
||||||
|
for item in value:
|
||||||
|
visit(item)
|
||||||
|
return
|
||||||
|
if isinstance(value, dict):
|
||||||
|
name = value.get("name")
|
||||||
|
if isinstance(name, (dict, list, tuple)):
|
||||||
|
visit(name)
|
||||||
|
return
|
||||||
|
append(name, value.get("order", "ASC"))
|
||||||
|
return
|
||||||
|
# 唯一索引在部分版本中会被解析成 ['mobile', 'ASC', 'tenant_id', 'ASC']。
|
||||||
|
if isinstance(value, str):
|
||||||
|
token = value.strip("`")
|
||||||
|
order = token.upper()
|
||||||
|
if order in ("ASC", "DESC"):
|
||||||
|
if order == "DESC" and keys and not keys[-1].endswith(" desc"):
|
||||||
|
keys[-1] = f"{keys[-1]} desc"
|
||||||
|
return
|
||||||
|
append(token)
|
||||||
|
|
||||||
|
visit(columns)
|
||||||
|
return keys
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def unique_index(ddl: Dict) -> Generator:
|
def unique_index(ddl: Dict) -> Generator:
|
||||||
if "constraints" in ddl and "uniques" in ddl["constraints"]:
|
if "constraints" in ddl and "uniques" in ddl["constraints"]:
|
||||||
|
|
@ -223,7 +289,9 @@ class Convertor(ABC):
|
||||||
for uk in uk_list:
|
for uk in uk_list:
|
||||||
table_name = ddl["table_name"]
|
table_name = ddl["table_name"]
|
||||||
uk_name = uk["constraint_name"]
|
uk_name = uk["constraint_name"]
|
||||||
uk_columns = uk["columns"]
|
uk_columns = Convertor.index_columns(uk["columns"])
|
||||||
|
if not uk_columns:
|
||||||
|
continue
|
||||||
yield table_name, uk_name, uk_columns
|
yield table_name, uk_name, uk_columns
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -381,7 +449,7 @@ class PostgreSQLConvertor(Convertor):
|
||||||
)
|
)
|
||||||
nullable = "NULL" if col["nullable"] else "NOT NULL"
|
nullable = "NULL" if col["nullable"] else "NOT NULL"
|
||||||
default = f"DEFAULT {col['default']}" if col["default"] is not None else ""
|
default = f"DEFAULT {col['default']}" if col["default"] is not None else ""
|
||||||
return f"{name} {full_type} {nullable} {default}"
|
return f"{self.escape_column_name(name)} {full_type} {nullable} {default}"
|
||||||
|
|
||||||
table_name = ddl["table_name"].lower()
|
table_name = ddl["table_name"].lower()
|
||||||
columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]]
|
columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]]
|
||||||
|
|
@ -406,7 +474,7 @@ CREATE TABLE {table_name} (
|
||||||
for column in table_ddl["columns"]:
|
for column in table_ddl["columns"]:
|
||||||
table_comment = column["comment"]
|
table_comment = column["comment"]
|
||||||
script += (
|
script += (
|
||||||
f"COMMENT ON COLUMN {table_ddl['table_name']}.{column['name']} IS '{table_comment}';"
|
f"COMMENT ON COLUMN {table_ddl['table_name']}.{self.escape_column_name(column['name'])} IS '{table_comment}';"
|
||||||
+ "\n"
|
+ "\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -435,6 +503,7 @@ CREATE TABLE {table_name} (
|
||||||
"""生成 insert 语句,以及根据最后的 insert id+1 生成 Sequence"""
|
"""生成 insert 语句,以及根据最后的 insert id+1 生成 Sequence"""
|
||||||
|
|
||||||
inserts = list(Convertor.inserts(table_name, self.content))
|
inserts = list(Convertor.inserts(table_name, self.content))
|
||||||
|
inserts = [self.escape_insert_columns(s) for s in inserts]
|
||||||
# 转换 MySQL 字符串转义为 PostgreSQL 格式:\\ -> \,\' -> ''
|
# 转换 MySQL 字符串转义为 PostgreSQL 格式:\\ -> \,\' -> ''
|
||||||
inserts = [re.sub(r"\\\\|\\'", lambda m: "\\" if m.group() == "\\\\" else "''", s) for s in inserts]
|
inserts = [re.sub(r"\\\\|\\'", lambda m: "\\" if m.group() == "\\\\" else "''", s) for s in inserts]
|
||||||
## 生成 insert 脚本
|
## 生成 insert 脚本
|
||||||
|
|
@ -482,6 +551,8 @@ INSERT INTO dual VALUES (1);
|
||||||
|
|
||||||
|
|
||||||
class OracleConvertor(Convertor):
|
class OracleConvertor(Convertor):
|
||||||
|
reserved_column_names = {"level", "size"}
|
||||||
|
|
||||||
def __init__(self, src):
|
def __init__(self, src):
|
||||||
super().__init__(src, "Oracle")
|
super().__init__(src, "Oracle")
|
||||||
|
|
||||||
|
|
@ -526,10 +597,8 @@ class OracleConvertor(Convertor):
|
||||||
# Oracle的 INSERT '' 不能通过NOT NULL校验,因此对文字类型字段覆写为 NULL
|
# Oracle的 INSERT '' 不能通过NOT NULL校验,因此对文字类型字段覆写为 NULL
|
||||||
nullable = "NULL" if type in ("varchar", "text", "longtext") else nullable
|
nullable = "NULL" if type in ("varchar", "text", "longtext") else nullable
|
||||||
default = f"DEFAULT {col['default']}" if col["default"] is not None else ""
|
default = f"DEFAULT {col['default']}" if col["default"] is not None else ""
|
||||||
# Oracle 中 size 不能作为字段名
|
|
||||||
field_name = '"size"' if name == "size" else name
|
|
||||||
# Oracle DEFAULT 定义在 NULLABLE 之前
|
# Oracle DEFAULT 定义在 NULLABLE 之前
|
||||||
return f"{field_name} {full_type} {default} {nullable}"
|
return f"{self.escape_column_name(name)} {full_type} {default} {nullable}"
|
||||||
|
|
||||||
table_name = ddl["table_name"].lower()
|
table_name = ddl["table_name"].lower()
|
||||||
columns = [f"{generate_column(col).strip()}" for col in ddl["columns"]]
|
columns = [f"{generate_column(col).strip()}" for col in ddl["columns"]]
|
||||||
|
|
@ -554,7 +623,7 @@ CREATE TABLE {table_name} (
|
||||||
for column in table_ddl["columns"]:
|
for column in table_ddl["columns"]:
|
||||||
table_comment = column["comment"]
|
table_comment = column["comment"]
|
||||||
script += (
|
script += (
|
||||||
f"COMMENT ON COLUMN {table_ddl['table_name']}.{column['name']} IS '{table_comment}';"
|
f"COMMENT ON COLUMN {table_ddl['table_name']}.{self.escape_column_name(column['name'])} IS '{table_comment}';"
|
||||||
+ "\n"
|
+ "\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -586,6 +655,7 @@ CREATE TABLE {table_name} (
|
||||||
"""拷贝 INSERT 语句"""
|
"""拷贝 INSERT 语句"""
|
||||||
inserts = []
|
inserts = []
|
||||||
for insert_script in Convertor.inserts(table_name, self.content):
|
for insert_script in Convertor.inserts(table_name, self.content):
|
||||||
|
insert_script = self.escape_insert_columns(insert_script)
|
||||||
# 对日期数据添加 TO_DATE 转换
|
# 对日期数据添加 TO_DATE 转换
|
||||||
insert_script = re.sub(
|
insert_script = re.sub(
|
||||||
r"('\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}')",
|
r"('\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}')",
|
||||||
|
|
@ -907,6 +977,8 @@ SET IDENTITY_INSERT {table_name.lower()} OFF;
|
||||||
|
|
||||||
|
|
||||||
class KingbaseConvertor(PostgreSQLConvertor):
|
class KingbaseConvertor(PostgreSQLConvertor):
|
||||||
|
reserved_column_names = {"level"}
|
||||||
|
|
||||||
def __init__(self, src):
|
def __init__(self, src):
|
||||||
super().__init__(src)
|
super().__init__(src)
|
||||||
self.db_type = "Kingbase"
|
self.db_type = "Kingbase"
|
||||||
|
|
@ -925,7 +997,7 @@ class KingbaseConvertor(PostgreSQLConvertor):
|
||||||
if full_type == "text":
|
if full_type == "text":
|
||||||
nullable = "NULL"
|
nullable = "NULL"
|
||||||
default = f"DEFAULT {col['default']}" if col["default"] is not None else ""
|
default = f"DEFAULT {col['default']}" if col["default"] is not None else ""
|
||||||
return f"{name} {full_type} {nullable} {default}"
|
return f"{self.escape_column_name(name)} {full_type} {nullable} {default}"
|
||||||
|
|
||||||
table_name = ddl["table_name"].lower()
|
table_name = ddl["table_name"].lower()
|
||||||
columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]]
|
columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]]
|
||||||
|
|
@ -945,23 +1017,32 @@ CREATE TABLE {table_name} (
|
||||||
|
|
||||||
|
|
||||||
class OpengaussConvertor(KingbaseConvertor):
|
class OpengaussConvertor(KingbaseConvertor):
|
||||||
|
reserved_column_names = set()
|
||||||
|
|
||||||
def __init__(self, src):
|
def __init__(self, src):
|
||||||
super().__init__(src)
|
super().__init__(src)
|
||||||
self.db_type = "OpenGauss"
|
self.db_type = "OpenGauss"
|
||||||
|
|
||||||
|
|
||||||
|
class HighGoConvertor(PostgreSQLConvertor):
|
||||||
|
def __init__(self, src):
|
||||||
|
super().__init__(src)
|
||||||
|
self.db_type = "HighGo"
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="芋道系统数据库转换工具")
|
parser = argparse.ArgumentParser(description="芋道系统数据库转换工具")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"type",
|
"type",
|
||||||
type=str,
|
type=str,
|
||||||
help="目标数据库类型",
|
help="目标数据库类型",
|
||||||
choices=["postgres", "oracle", "sqlserver", "dm8", "kingbase", "opengauss"],
|
choices=["postgres", "oracle", "sqlserver", "dm8", "kingbase", "opengauss", "highgo"],
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"path",
|
"path",
|
||||||
type=str,
|
type=str,
|
||||||
help="源数据库脚本路径",
|
help="源数据库脚本路径",
|
||||||
|
nargs="?",
|
||||||
default="../mysql/ruoyi-vue-pro.sql"
|
default="../mysql/ruoyi-vue-pro.sql"
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
@ -980,6 +1061,8 @@ def main():
|
||||||
convertor = KingbaseConvertor(sql_file)
|
convertor = KingbaseConvertor(sql_file)
|
||||||
elif args.type == "opengauss":
|
elif args.type == "opengauss":
|
||||||
convertor = OpengaussConvertor(sql_file)
|
convertor = OpengaussConvertor(sql_file)
|
||||||
|
elif args.type == "highgo":
|
||||||
|
convertor = HighGoConvertor(sql_file)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(f"不支持目标数据库类型: {args.type}")
|
raise NotImplementedError(f"不支持目标数据库类型: {args.type}")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ volumes:
|
||||||
dm8: { }
|
dm8: { }
|
||||||
kingbase: { }
|
kingbase: { }
|
||||||
opengauss: { }
|
opengauss: { }
|
||||||
|
highgo: { }
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mysql:
|
mysql:
|
||||||
|
|
@ -21,7 +22,7 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- mysql:/var/lib/mysql/
|
- mysql:/var/lib/mysql/
|
||||||
# 注入初始化脚本
|
# 注入初始化脚本
|
||||||
- ./mysql/ruoyi-vue-pro.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
- ../mysql/ruoyi-vue-pro.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
||||||
command:
|
command:
|
||||||
--default-authentication-plugin=mysql_native_password
|
--default-authentication-plugin=mysql_native_password
|
||||||
--character-set-server=utf8mb4
|
--character-set-server=utf8mb4
|
||||||
|
|
@ -132,3 +133,15 @@ services:
|
||||||
- opengauss:/var/lib/opengauss
|
- opengauss:/var/lib/opengauss
|
||||||
- ../opengauss/ruoyi-vue-pro.sql:/tmp/schema.sql:ro
|
- ../opengauss/ruoyi-vue-pro.sql:/tmp/schema.sql:ro
|
||||||
# docker compose exec opengauss bash -c '/usr/local/opengauss/bin/gsql -U $GS_USERNAME -W $GS_PASSWORD -d postgres -f /tmp/schema.sql'
|
# docker compose exec opengauss bash -c '/usr/local/opengauss/bin/gsql -U $GS_USERNAME -W $GS_PASSWORD -d postgres -f /tmp/schema.sql'
|
||||||
|
|
||||||
|
highgo:
|
||||||
|
# 使用瀚高官方提供的 Docker 镜像,加载后打成本地标签:
|
||||||
|
# docker tag <image>:<tag> highgo:local
|
||||||
|
image: highgo:local
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "5866:5866"
|
||||||
|
volumes:
|
||||||
|
- highgo:/home/highgo/hgdb/data
|
||||||
|
- ../highgo/quartz.sql:/tmp/quartz.sql:ro
|
||||||
|
- ../highgo/ruoyi-vue-pro.sql:/tmp/schema.sql:ro
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<revision>2026.04-jdk8-SNAPSHOT</revision>
|
<revision>2026.05-jdk8-SNAPSHOT</revision>
|
||||||
<flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
|
<flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
|
||||||
<!-- 统一依赖管理 -->
|
<!-- 统一依赖管理 -->
|
||||||
<spring.framework.version>5.3.39</spring.framework.version>
|
<spring.framework.version>5.3.39</spring.framework.version>
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
<mybatis-plus-join.version>1.5.7</mybatis-plus-join.version>
|
<mybatis-plus-join.version>1.5.7</mybatis-plus-join.version>
|
||||||
<dynamic-datasource.version>4.5.0</dynamic-datasource.version>
|
<dynamic-datasource.version>4.5.0</dynamic-datasource.version>
|
||||||
<easy-trans.version>3.0.6</easy-trans.version>
|
<easy-trans.version>3.0.6</easy-trans.version>
|
||||||
<redisson.version>4.3.1</redisson.version>
|
<redisson.version>4.4.0</redisson.version>
|
||||||
<dm8.jdbc.version>8.1.3.140</dm8.jdbc.version>
|
<dm8.jdbc.version>8.1.3.140</dm8.jdbc.version>
|
||||||
<kingbase.jdbc.version>9.0.1.jre7</kingbase.jdbc.version>
|
<kingbase.jdbc.version>9.0.1.jre7</kingbase.jdbc.version>
|
||||||
<opengauss.jdbc.version>7.0.0-RC3-og</opengauss.jdbc.version>
|
<opengauss.jdbc.version>7.0.0-RC3-og</opengauss.jdbc.version>
|
||||||
|
|
@ -59,6 +59,8 @@
|
||||||
<!-- 工具类相关 -->
|
<!-- 工具类相关 -->
|
||||||
<anji-plus-captcha.version>1.4.0</anji-plus-captcha.version>
|
<anji-plus-captcha.version>1.4.0</anji-plus-captcha.version>
|
||||||
<jsoup.version>1.22.2</jsoup.version>
|
<jsoup.version>1.22.2</jsoup.version>
|
||||||
|
<sensitive-word.version>0.29.5</sensitive-word.version>
|
||||||
|
<pinyin4j.version>2.5.1</pinyin4j.version>
|
||||||
<lombok.version>1.18.46</lombok.version>
|
<lombok.version>1.18.46</lombok.version>
|
||||||
<mapstruct.version>1.6.3</mapstruct.version>
|
<mapstruct.version>1.6.3</mapstruct.version>
|
||||||
<hutool-5.version>5.8.44</hutool-5.version>
|
<hutool-5.version>5.8.44</hutool-5.version>
|
||||||
|
|
@ -74,7 +76,7 @@
|
||||||
<ip2region.version>2.7.0</ip2region.version>
|
<ip2region.version>2.7.0</ip2region.version>
|
||||||
<bizlog-sdk.version>3.0.6</bizlog-sdk.version>
|
<bizlog-sdk.version>3.0.6</bizlog-sdk.version>
|
||||||
<reflections.version>0.10.2</reflections.version>
|
<reflections.version>0.10.2</reflections.version>
|
||||||
<netty.version>4.2.12.Final</netty.version>
|
<netty.version>4.2.14.Final</netty.version>
|
||||||
<mqtt.version>1.2.5</mqtt.version>
|
<mqtt.version>1.2.5</mqtt.version>
|
||||||
<vertx.version>4.5.26</vertx.version>
|
<vertx.version>4.5.26</vertx.version>
|
||||||
<okhttp.version>4.12.0</okhttp.version>
|
<okhttp.version>4.12.0</okhttp.version>
|
||||||
|
|
@ -86,10 +88,11 @@
|
||||||
<awssdk.version>2.44.0</awssdk.version>
|
<awssdk.version>2.44.0</awssdk.version>
|
||||||
<justauth.version>1.16.7</justauth.version>
|
<justauth.version>1.16.7</justauth.version>
|
||||||
<justauth-starter.version>1.4.0</justauth-starter.version>
|
<justauth-starter.version>1.4.0</justauth-starter.version>
|
||||||
<jimureport.version>2.3.2</jimureport.version>
|
<jimureport.version>2.3.4</jimureport.version>
|
||||||
<jimubi.version>2.3.2</jimubi.version>
|
<jimubi.version>2.3.2</jimubi.version>
|
||||||
<weixin-java.version>4.8.2-20260501.180637</weixin-java.version>
|
<weixin-java.version>4.8.2-20260501.180637</weixin-java.version>
|
||||||
<alipay-sdk-java.version>4.40.771.ALL</alipay-sdk-java.version>
|
<bouncycastle.version>1.80</bouncycastle.version>
|
||||||
|
<alipay-sdk-java.version>4.40.806.ALL</alipay-sdk-java.version>
|
||||||
<!-- 专属于 JDK8 安全漏洞升级 -->
|
<!-- 专属于 JDK8 安全漏洞升级 -->
|
||||||
<logback.version>1.2.13</logback.version> <!-- 无法使用 1.3.X 版本,启动会报错 -->
|
<logback.version>1.2.13</logback.version> <!-- 无法使用 1.3.X 版本,启动会报错 -->
|
||||||
</properties>
|
</properties>
|
||||||
|
|
@ -624,6 +627,18 @@
|
||||||
<version>${jsoup.version}</version>
|
<version>${jsoup.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.houbb</groupId>
|
||||||
|
<artifactId>sensitive-word</artifactId> <!-- 敏感词检测:trie 树高效匹配 -->
|
||||||
|
<version>${sensitive-word.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.belerweb</groupId>
|
||||||
|
<artifactId>pinyin4j</artifactId> <!-- 汉字转拼音:作为 hutool PinyinUtil 的底层引擎 -->
|
||||||
|
<version>${pinyin4j.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.reflections</groupId>
|
<groupId>org.reflections</groupId>
|
||||||
<artifactId>reflections</artifactId>
|
<artifactId>reflections</artifactId>
|
||||||
|
|
@ -735,6 +750,24 @@
|
||||||
</exclusions>
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 锁定 weixin-java 传递依赖,避免 Maven 版本范围自动升级到 1.80.2 后 Fat Jar 启动失败。
|
||||||
|
反馈:https://t.zsxq.com/pCVBo -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bouncycastle</groupId>
|
||||||
|
<artifactId>bcprov-jdk18on</artifactId>
|
||||||
|
<version>${bouncycastle.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bouncycastle</groupId>
|
||||||
|
<artifactId>bcutil-jdk18on</artifactId>
|
||||||
|
<version>${bouncycastle.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bouncycastle</groupId>
|
||||||
|
<artifactId>bcpkix-jdk18on</artifactId>
|
||||||
|
<version>${bouncycastle.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.binarywang</groupId>
|
<groupId>com.github.binarywang</groupId>
|
||||||
<artifactId>weixin-java-pay</artifactId>
|
<artifactId>weixin-java-pay</artifactId>
|
||||||
|
|
|
||||||
|
|
@ -53,8 +53,8 @@
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>jakarta.servlet</groupId>
|
<groupId>javax.servlet</groupId>
|
||||||
<artifactId>jakarta.servlet-api</artifactId>
|
<artifactId>javax.servlet-api</artifactId>
|
||||||
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
|
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
|
@ -125,8 +125,8 @@
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>jakarta.validation</groupId>
|
<groupId>javax.validation</groupId>
|
||||||
<artifactId>jakarta.validation-api</artifactId>
|
<artifactId>validation-api</artifactId>
|
||||||
<scope>provided</scope> <!-- 设置为 provided,主要是 PageParam 使用到 -->
|
<scope>provided</scope> <!-- 设置为 provided,主要是 PageParam 使用到 -->
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,22 @@ public class CollectionUtils {
|
||||||
return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet());
|
return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <T, U> Set<U> convertLinkedSet(Collection<T> from, Function<T, U> func) {
|
||||||
|
if (CollUtil.isEmpty(from)) {
|
||||||
|
return new LinkedHashSet<>();
|
||||||
|
}
|
||||||
|
return from.stream().map(func).filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T, U> Set<U> convertLinkedSet(Collection<T> from, Function<T, U> func, Predicate<T> filter) {
|
||||||
|
if (CollUtil.isEmpty(from)) {
|
||||||
|
return new LinkedHashSet<>();
|
||||||
|
}
|
||||||
|
return from.stream().filter(filter).map(func).filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||||
|
}
|
||||||
|
|
||||||
public static <T, K> Map<K, T> convertMapByFilter(Collection<T> from, Predicate<T> filter, Function<T, K> keyFunc) {
|
public static <T, K> Map<K, T> convertMapByFilter(Collection<T> from, Predicate<T> filter, Function<T, K> keyFunc) {
|
||||||
if (CollUtil.isEmpty(from)) {
|
if (CollUtil.isEmpty(from)) {
|
||||||
return new HashMap<>();
|
return new HashMap<>();
|
||||||
|
|
@ -372,4 +388,14 @@ public class CollectionUtils {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把单元素 head 与集合 tail 合并成新 List(head 在前,tail 顺序保留)
|
||||||
|
*/
|
||||||
|
public static <T> List<T> of(T head, Collection<T> tail) {
|
||||||
|
List<T> list = new ArrayList<>();
|
||||||
|
list.add(head);
|
||||||
|
CollUtil.addAll(list, tail);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -236,6 +236,23 @@ public class LocalDateTimeUtils {
|
||||||
return LocalDateTime.now().with(TemporalAdjusters.firstDayOfYear()).with(LocalTime.MIN);
|
return LocalDateTime.now().with(TemporalAdjusters.firstDayOfYear()).with(LocalTime.MIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最近 N 天的 0 点时刻序列(升序,含今天)
|
||||||
|
* <p>
|
||||||
|
* 例:getLatestDays(3) 返回 [前天 00:00, 昨天 00:00, 今天 00:00]
|
||||||
|
*
|
||||||
|
* @param days 天数(含今天)
|
||||||
|
* @return 升序的 LocalDateTime 列表
|
||||||
|
*/
|
||||||
|
public static List<LocalDateTime> getLatestDays(int days) {
|
||||||
|
LocalDateTime today = getToday();
|
||||||
|
List<LocalDateTime> dates = new ArrayList<>(days);
|
||||||
|
for (int i = days - 1; i >= 0; i--) {
|
||||||
|
dates.add(today.minusDays(i));
|
||||||
|
}
|
||||||
|
return dates;
|
||||||
|
}
|
||||||
|
|
||||||
public static List<LocalDateTime[]> getDateRangeList(LocalDateTime startTime,
|
public static List<LocalDateTime[]> getDateRangeList(LocalDateTime startTime,
|
||||||
LocalDateTime endTime,
|
LocalDateTime endTime,
|
||||||
Integer interval) {
|
Integer interval) {
|
||||||
|
|
@ -302,6 +319,21 @@ public class LocalDateTimeUtils {
|
||||||
return timeRanges;
|
return timeRanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取从开始日期起的日期列表
|
||||||
|
*
|
||||||
|
* @param startDate 开始日期
|
||||||
|
* @param days 天数
|
||||||
|
* @return 日期列表,包含开始日期
|
||||||
|
*/
|
||||||
|
public static List<LocalDate> getDateList(LocalDate startDate, int days) {
|
||||||
|
List<LocalDate> dateList = new ArrayList<>(days);
|
||||||
|
for (int i = 0; i < days; i++) {
|
||||||
|
dateList.add(startDate.plusDays(i));
|
||||||
|
}
|
||||||
|
return dateList;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化时间范围
|
* 格式化时间范围
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import lombok.SneakyThrows;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.util.UriComponents;
|
import org.springframework.web.util.UriComponents;
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
import org.springframework.web.util.UriUtils;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
|
@ -59,11 +60,61 @@ public class HttpUtils {
|
||||||
*/
|
*/
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public static String decodeUrlPath(String path) {
|
public static String decodeUrlPath(String path) {
|
||||||
|
if (StrUtil.isEmpty(path)) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
// 先将 + 替换为 %2B,避免被 URLDecoder 解码为空格
|
// 先将 + 替换为 %2B,避免被 URLDecoder 解码为空格
|
||||||
String encoded = path.replace("+", "%2B");
|
String encoded = path.replace("+", "%2B");
|
||||||
return URLDecoder.decode(encoded, StandardCharsets.UTF_8.name());
|
return URLDecoder.decode(encoded, StandardCharsets.UTF_8.name());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编码 URL 路径,按路径段编码,保留 / 分隔符
|
||||||
|
*
|
||||||
|
* @param path URL 路径,例如 20250602/xxx.pdf
|
||||||
|
* @return 编码后的路径
|
||||||
|
*/
|
||||||
|
public static String encodeUrlPath(String path) {
|
||||||
|
if (StrUtil.isEmpty(path)) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
String[] segments = path.split(StrUtil.SLASH, -1);
|
||||||
|
StringBuilder result = new StringBuilder(path.length());
|
||||||
|
for (int i = 0; i < segments.length; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
result.append(StrUtil.SLASH);
|
||||||
|
}
|
||||||
|
result.append(encodeUrlPathSegment(segments[i]));
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编码 URL 路径段
|
||||||
|
*
|
||||||
|
* @param segment URL 路径段
|
||||||
|
* @return 编码后的路径段
|
||||||
|
*/
|
||||||
|
public static String encodeUrlPathSegment(String segment) {
|
||||||
|
return UriUtils.encodePathSegment(segment, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String removeUrlPathQueryAndFragment(String path) {
|
||||||
|
if (StrUtil.isEmpty(path)) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
int endIndex = path.length();
|
||||||
|
int queryIndex = path.indexOf('?');
|
||||||
|
if (queryIndex >= 0) {
|
||||||
|
endIndex = queryIndex;
|
||||||
|
}
|
||||||
|
int fragmentIndex = path.indexOf('#');
|
||||||
|
if (fragmentIndex >= 0 && fragmentIndex < endIndex) {
|
||||||
|
endIndex = fragmentIndex;
|
||||||
|
}
|
||||||
|
return path.substring(0, endIndex);
|
||||||
|
}
|
||||||
|
|
||||||
public static String replaceUrlQuery(String url, String key, String value) {
|
public static String replaceUrlQuery(String url, String key, String value) {
|
||||||
UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset());
|
UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset());
|
||||||
// 先移除;再添加
|
// 先移除;再添加
|
||||||
|
|
@ -204,4 +255,14 @@ public class HttpUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket URL 切换成 HTTP URL:ws:// → http://;wss:// → https://;其它格式原样保留
|
||||||
|
*
|
||||||
|
* @param url 原始 URL
|
||||||
|
* @return 切换协议后的 URL
|
||||||
|
*/
|
||||||
|
public static String wsUrlToHttp(String url) {
|
||||||
|
return StrUtil.startWithIgnoreCase(url, "ws") ? "http" + url.substring(2) : url;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import java.lang.reflect.Type;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON 工具类
|
* JSON 工具类
|
||||||
|
|
@ -173,6 +174,23 @@ public class JsonUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 JSON 字符串成 Map,空字符串或解析失败返回 null
|
||||||
|
*
|
||||||
|
* @param text JSON 字符串
|
||||||
|
* @return Map 对象
|
||||||
|
*/
|
||||||
|
public static Map<String, Object> parseMap(String text) {
|
||||||
|
if (StrUtil.isEmpty(text)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.readValue(text, new TypeReference<Map<String, Object>>() {});
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析 JSON 字符串成指定类型的对象,如果解析失败,则返回 null
|
* 解析 JSON 字符串成指定类型的对象,如果解析失败,则返回 null
|
||||||
*
|
*
|
||||||
|
|
@ -235,6 +253,14 @@ public class JsonUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getText(JsonNode node, String fieldName) {
|
||||||
|
if (node == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JsonNode value = node.get(fieldName);
|
||||||
|
return value != null && !value.isNull() ? value.asText() : null;
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isJson(String text) {
|
public static boolean isJson(String text) {
|
||||||
return JSONUtil.isTypeJSON(text);
|
return JSONUtil.isTypeJSON(text);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.common.util.string;
|
||||||
import cn.hutool.core.text.StrPool;
|
import cn.hutool.core.text.StrPool;
|
||||||
import cn.hutool.core.util.ArrayUtil;
|
import cn.hutool.core.util.ArrayUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.extra.pinyin.PinyinUtil;
|
||||||
import org.aspectj.lang.JoinPoint;
|
import org.aspectj.lang.JoinPoint;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
@ -78,6 +79,25 @@ public class StrUtils {
|
||||||
.collect(Collectors.joining("\n"));
|
.collect(Collectors.joining("\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转小写拼音,字之间以空格分隔,便于调用方按需拼接 / 取首字母 / 拼音搜索
|
||||||
|
*
|
||||||
|
* 例:「老张」→ "lao zhang"、「ZhangSan」→ "zhangsan"
|
||||||
|
* 英文 / 数字 / 符号原样返回,空值返回 null
|
||||||
|
*
|
||||||
|
* 注意:底层依赖 hutool-extra 的 {@link PinyinUtil},需要业务模块自行引入拼音引擎依赖
|
||||||
|
* (pinyin4j / TinyPinyin / Bopomofo4j 任选其一),否则运行时会抛 NoClassDefFoundError
|
||||||
|
*
|
||||||
|
* @param str 字符串
|
||||||
|
* @return 拼音串(保留空格分隔)
|
||||||
|
*/
|
||||||
|
public static String toPinyin(String str) {
|
||||||
|
if (StrUtil.isBlank(str)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return PinyinUtil.getPinyin(str);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 拼接方法的参数
|
* 拼接方法的参数
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,36 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
*/
|
*/
|
||||||
public class HttpUtilsTest {
|
public class HttpUtilsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEncodeUrlPath() {
|
||||||
|
// 准备参数
|
||||||
|
String path = "avatar/中文 100%+文件.jpg";
|
||||||
|
|
||||||
|
// 调用
|
||||||
|
String result = HttpUtils.encodeUrlPath(path);
|
||||||
|
|
||||||
|
// 断言
|
||||||
|
assertEquals("avatar/%E4%B8%AD%E6%96%87%20100%25+%E6%96%87%E4%BB%B6.jpg", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDecodeUrlPath() {
|
||||||
|
// 准备参数:+ 是路径字符,不应该按 query parameter 语义解码为空格
|
||||||
|
String path = "avatar/%E4%B8%AD%E6%96%87%20100%25+%E6%96%87%E4%BB%B6.jpg";
|
||||||
|
|
||||||
|
// 调用
|
||||||
|
String result = HttpUtils.decodeUrlPath(path);
|
||||||
|
|
||||||
|
// 断言
|
||||||
|
assertEquals("avatar/中文 100%+文件.jpg", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRemoveUrlPathQueryAndFragment() {
|
||||||
|
assertEquals("avatar/test.jpg", HttpUtils.removeUrlPathQueryAndFragment("avatar/test.jpg?token=1#preview"));
|
||||||
|
assertEquals("avatar/test.jpg", HttpUtils.removeUrlPathQueryAndFragment("avatar/test.jpg#preview?token=1"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReplaceUrlQuery_replace() {
|
public void testReplaceUrlQuery_replace() {
|
||||||
// 准备参数
|
// 准备参数
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package cn.iocoder.yudao.framework.tenant.config;
|
package cn.iocoder.yudao.framework.tenant.config;
|
||||||
|
|
||||||
import cn.hutool.extra.spring.SpringUtil;
|
|
||||||
import cn.iocoder.yudao.framework.common.biz.system.tenant.TenantCommonApi;
|
import cn.iocoder.yudao.framework.common.biz.system.tenant.TenantCommonApi;
|
||||||
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
|
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
|
||||||
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
|
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
|
||||||
|
|
@ -23,6 +22,7 @@ import cn.iocoder.yudao.framework.web.config.WebProperties;
|
||||||
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
|
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
||||||
|
import javax.annotation.Resource;
|
||||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
|
@ -45,11 +45,7 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||||
import org.springframework.web.util.pattern.PathPattern;
|
import org.springframework.web.util.pattern.PathPattern;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import java.util.*;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||||
|
|
||||||
|
|
@ -63,13 +59,6 @@ public class YudaoTenantAutoConfiguration {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public TenantFrameworkService tenantFrameworkService(TenantCommonApi tenantApi) {
|
public TenantFrameworkService tenantFrameworkService(TenantCommonApi tenantApi) {
|
||||||
// 参见 https://gitee.com/zhijiantianya/yudao-cloud/issues/IC6YZF
|
|
||||||
try {
|
|
||||||
TenantCommonApi tenantApiImpl = SpringUtil.getBean("tenantApiImpl", TenantCommonApi.class);
|
|
||||||
if (tenantApiImpl != null) {
|
|
||||||
tenantApi = tenantApiImpl;
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
return new TenantFrameworkServiceImpl(tenantApi);
|
return new TenantFrameworkServiceImpl(tenantApi);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -167,14 +156,9 @@ public class YudaoTenantAutoConfiguration {
|
||||||
|
|
||||||
// ========== MQ ==========
|
// ========== MQ ==========
|
||||||
|
|
||||||
/**
|
@Configuration(proxyBeanMethods = false)
|
||||||
* 多租户 Redis 消息队列的配置类
|
@ConditionalOnClass(name = "cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor")
|
||||||
*
|
public static class TenantRedisMQConfiguration {
|
||||||
* 为什么要单独一个配置类呢?如果直接把 TenantRedisMessageInterceptor Bean 的初始化放外面,会报 RedisMessageInterceptor 类不存在的错误
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
@ConditionalOnClass(name = "cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate")
|
|
||||||
public static class TenantRedisMQAutoConfiguration {
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() {
|
public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() {
|
||||||
|
|
@ -183,24 +167,26 @@ public class YudaoTenantAutoConfiguration {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Configuration(proxyBeanMethods = false)
|
||||||
@ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate")
|
@ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate")
|
||||||
public TenantRabbitMQInitializer tenantRabbitMQInitializer() {
|
public static class TenantRabbitMQConfiguration {
|
||||||
return new TenantRabbitMQInitializer();
|
|
||||||
|
@Bean
|
||||||
|
public TenantRabbitMQInitializer tenantRabbitMQInitializer() {
|
||||||
|
return new TenantRabbitMQInitializer();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Configuration(proxyBeanMethods = false)
|
||||||
@ConditionalOnClass(name = "org.apache.rocketmq.spring.core.RocketMQTemplate")
|
@ConditionalOnClass(name = "org.apache.rocketmq.spring.core.RocketMQTemplate")
|
||||||
public TenantRocketMQInitializer tenantRocketMQInitializer() {
|
public static class TenantRocketMQConfiguration {
|
||||||
return new TenantRocketMQInitializer();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== Job ==========
|
@Bean
|
||||||
|
public TenantRocketMQInitializer tenantRocketMQInitializer() {
|
||||||
|
return new TenantRocketMQInitializer();
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
@ConditionalOnClass(name = "com.xxl.job.core.handler.annotation.XxlJob")
|
|
||||||
public TenantJobAspect tenantJobAspect(TenantFrameworkService tenantFrameworkService) {
|
|
||||||
return new TenantJobAspect(tenantFrameworkService);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Redis ==========
|
// ========== Redis ==========
|
||||||
|
|
@ -216,7 +202,25 @@ public class YudaoTenantAutoConfiguration {
|
||||||
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory,
|
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory,
|
||||||
BatchStrategies.scan(yudaoCacheProperties.getRedisScanBatchSize()));
|
BatchStrategies.scan(yudaoCacheProperties.getRedisScanBatchSize()));
|
||||||
// 创建 TenantRedisCacheManager 对象
|
// 创建 TenantRedisCacheManager 对象
|
||||||
return new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration, tenantProperties.getIgnoreCaches());
|
TenantRedisCacheManager cacheManager = new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration,
|
||||||
|
tenantProperties.getIgnoreCaches());
|
||||||
|
// 开启事务感知:@Transactional 方法内的 @CacheEvict / @CachePut 自动延迟到 afterCommit,
|
||||||
|
// 避免事务未提交就清缓存被并发读穿写脏值;无事务时立即生效,行为不变
|
||||||
|
cacheManager.setTransactionAware(true);
|
||||||
|
return cacheManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Job ==========
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ConditionalOnClass(name = "com.xxl.job.core.context.XxlJobContext")
|
||||||
|
public static class TenantJobConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public TenantJobAspect tenantJobAspect(TenantFrameworkService tenantFrameworkService) {
|
||||||
|
return new TenantJobAspect(tenantFrameworkService);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,8 @@
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>jakarta.servlet</groupId>
|
<groupId>javax.servlet</groupId>
|
||||||
<artifactId>jakarta.servlet-api</artifactId>
|
<artifactId>javax.servlet-api</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- RPC 相关 -->
|
<!-- RPC 相关 -->
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,8 @@
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>jakarta.servlet</groupId>
|
<groupId>javax.servlet</groupId>
|
||||||
<artifactId>jakarta.servlet-api</artifactId>
|
<artifactId>javax.servlet-api</artifactId>
|
||||||
<scope>provided</scope> <!-- 设置为 provided,只有 ExcelUtils 使用 -->
|
<scope>provided</scope> <!-- 设置为 provided,只有 ExcelUtils 使用 -->
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,8 @@
|
||||||
|
|
||||||
<!-- 工具类相关 -->
|
<!-- 工具类相关 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>jakarta.validation</groupId>
|
<groupId>javax.validation</groupId>
|
||||||
<artifactId>jakarta.validation-api</artifactId>
|
<artifactId>validation-api</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,8 @@
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>jakarta.servlet</groupId>
|
<groupId>javax.servlet</groupId>
|
||||||
<artifactId>jakarta.servlet-api</artifactId>
|
<artifactId>javax.servlet-api</artifactId>
|
||||||
<scope>provided</scope> <!-- 设置为 provided,只有 TraceFilter 使用 -->
|
<scope>provided</scope> <!-- 设置为 provided,只有 TraceFilter 使用 -->
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessag
|
||||||
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
|
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.redisson.api.RedissonClient;
|
import org.redisson.api.RedissonClient;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
|
@ -70,7 +69,8 @@ public class YudaoRedisMQConsumerAutoConfiguration {
|
||||||
public RedisPendingMessageResendJob redisPendingMessageResendJob(List<AbstractRedisStreamMessageListener<?>> listeners,
|
public RedisPendingMessageResendJob redisPendingMessageResendJob(List<AbstractRedisStreamMessageListener<?>> listeners,
|
||||||
RedisMQTemplate redisTemplate,
|
RedisMQTemplate redisTemplate,
|
||||||
RedissonClient redissonClient) {
|
RedissonClient redissonClient) {
|
||||||
return new RedisPendingMessageResendJob(listeners, redisTemplate, redissonClient);
|
return new RedisPendingMessageResendJob(listeners, redisTemplate, redissonClient,
|
||||||
|
RedisPendingMessageResendJob.DEFAULT_RESEND_LOCK_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -81,7 +81,8 @@ public class YudaoRedisMQConsumerAutoConfiguration {
|
||||||
public RedisStreamMessageCleanupJob redisStreamMessageCleanupJob(List<AbstractRedisStreamMessageListener<?>> listeners,
|
public RedisStreamMessageCleanupJob redisStreamMessageCleanupJob(List<AbstractRedisStreamMessageListener<?>> listeners,
|
||||||
RedisMQTemplate redisTemplate,
|
RedisMQTemplate redisTemplate,
|
||||||
RedissonClient redissonClient) {
|
RedissonClient redissonClient) {
|
||||||
return new RedisStreamMessageCleanupJob(listeners, redisTemplate, redissonClient);
|
return new RedisStreamMessageCleanupJob(listeners, redisTemplate, redissonClient,
|
||||||
|
RedisStreamMessageCleanupJob.DEFAULT_CLEANUP_LOCK_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,9 @@ import java.util.Objects;
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class RedisPendingMessageResendJob {
|
public class RedisPendingMessageResendJob {
|
||||||
|
|
||||||
private static final String LOCK_KEY = "redis:stream:pending-message-resend:lock";
|
public static final String DEFAULT_RESEND_LOCK_KEY = "redis:stream:pending-message-resend:lock";
|
||||||
|
|
||||||
|
public static final String IOT_RESEND_LOCK_KEY = "redis:stream:pending-message-resend:lock:iot";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息超时时间,默认 5 分钟
|
* 消息超时时间,默认 5 分钟
|
||||||
|
|
@ -36,22 +38,26 @@ public class RedisPendingMessageResendJob {
|
||||||
private final List<AbstractRedisStreamMessageListener<?>> listeners;
|
private final List<AbstractRedisStreamMessageListener<?>> listeners;
|
||||||
private final RedisMQTemplate redisTemplate;
|
private final RedisMQTemplate redisTemplate;
|
||||||
private final RedissonClient redissonClient;
|
private final RedissonClient redissonClient;
|
||||||
|
private final String resendLockKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 一分钟执行一次,这里选择每分钟的 35 秒执行,是为了避免整点任务过多的问题
|
* 一分钟执行一次,这里选择每分钟的 35 秒执行,是为了避免整点任务过多的问题
|
||||||
*/
|
*/
|
||||||
@Scheduled(cron = "35 * * * * ?")
|
@Scheduled(cron = "35 * * * * ?")
|
||||||
public void messageResend() {
|
public void messageResend() {
|
||||||
RLock lock = redissonClient.getLock(LOCK_KEY);
|
RLock lock = redissonClient.getLock(resendLockKey);
|
||||||
// 尝试加锁
|
|
||||||
if (lock.tryLock()) {
|
if (lock.tryLock()) {
|
||||||
try {
|
try {
|
||||||
execute();
|
execute();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.error("[messageResend][执行异常]", ex);
|
log.error("[messageResend][执行异常][lockKey={}]", resendLockKey, ex);
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
if (lock.isHeldByCurrentThread()) {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
log.debug("[messageResend][未获取到锁,跳过本轮][lockKey={}]", resendLockKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,16 @@ import java.util.List;
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class RedisStreamMessageCleanupJob {
|
public class RedisStreamMessageCleanupJob {
|
||||||
|
|
||||||
private static final String LOCK_KEY = "redis:stream:message-cleanup:lock";
|
/**
|
||||||
|
* 业务 MQ(Spring 容器内 AbstractRedisStreamMessageListener)清理任务使用的分布式锁
|
||||||
|
*/
|
||||||
|
public static final String DEFAULT_CLEANUP_LOCK_KEY = "redis:stream:message-cleanup:lock";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IoT Redis 总线清理任务使用的分布式锁(须与 {@link #DEFAULT_CLEANUP_LOCK_KEY} 区分,否则会共抢一把锁,
|
||||||
|
* 同一时刻只有一侧能执行 XTRIM,另一侧 Stream 可能无限积压)
|
||||||
|
*/
|
||||||
|
public static final String IOT_CLEANUP_LOCK_KEY = "redis:stream:message-cleanup:lock:iot";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保留的消息数量,默认保留最近 10000 条消息
|
* 保留的消息数量,默认保留最近 10000 条消息
|
||||||
|
|
@ -33,22 +42,29 @@ public class RedisStreamMessageCleanupJob {
|
||||||
private final List<AbstractRedisStreamMessageListener<?>> listeners;
|
private final List<AbstractRedisStreamMessageListener<?>> listeners;
|
||||||
private final RedisMQTemplate redisTemplate;
|
private final RedisMQTemplate redisTemplate;
|
||||||
private final RedissonClient redissonClient;
|
private final RedissonClient redissonClient;
|
||||||
|
/**
|
||||||
|
* Redisson 锁键(多 Bean 注册清理任务时必须各不相同)
|
||||||
|
*/
|
||||||
|
private final String cleanupLockKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 每小时执行一次清理任务
|
* 每小时执行一次清理任务
|
||||||
*/
|
*/
|
||||||
@Scheduled(cron = "0 0 * * * ?")
|
@Scheduled(cron = "0 0 * * * ?")
|
||||||
public void cleanup() {
|
public void cleanup() {
|
||||||
RLock lock = redissonClient.getLock(LOCK_KEY);
|
RLock lock = redissonClient.getLock(cleanupLockKey);
|
||||||
// 尝试加锁
|
|
||||||
if (lock.tryLock()) {
|
if (lock.tryLock()) {
|
||||||
try {
|
try {
|
||||||
execute();
|
execute();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.error("[cleanup][执行异常]", ex);
|
log.error("[cleanup][执行异常][lockKey={}]", cleanupLockKey, ex);
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
if (lock.isHeldByCurrentThread()) {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
log.debug("[cleanup][未获取到锁,跳过本轮][lockKey={}]", cleanupLockKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,8 +75,8 @@ public class RedisStreamMessageCleanupJob {
|
||||||
StreamOperations<String, Object, Object> ops = redisTemplate.getRedisTemplate().opsForStream();
|
StreamOperations<String, Object, Object> ops = redisTemplate.getRedisTemplate().opsForStream();
|
||||||
listeners.forEach(listener -> {
|
listeners.forEach(listener -> {
|
||||||
try {
|
try {
|
||||||
// 使用 XTRIM 命令清理消息,只保留最近的 MAX_LEN 条消息
|
// 使用 XTRIM MAXLEN 精确裁剪(approximate=false),避免 ~ 模式下长期明显高于上限
|
||||||
Long trimCount = ops.trim(listener.getStreamKey(), MAX_COUNT, true);
|
Long trimCount = ops.trim(listener.getStreamKey(), MAX_COUNT, false);
|
||||||
if (trimCount != null && trimCount > 0) {
|
if (trimCount != null && trimCount > 0) {
|
||||||
log.info("[execute][Stream({}) 清理消息数量({})]", listener.getStreamKey(), trimCount);
|
log.info("[execute][Stream({}) 清理消息数量({})]", listener.getStreamKey(), trimCount);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,13 @@
|
||||||
<groupId>com.fhs-opensource</groupId>
|
<groupId>com.fhs-opensource</groupId>
|
||||||
<artifactId>easy-trans-mybatis-plus-extend</artifactId>
|
<artifactId>easy-trans-mybatis-plus-extend</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Test 测试相关 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|
|
||||||
|
|
@ -20,51 +20,49 @@ public enum DbTypeEnum {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* H2
|
* H2
|
||||||
*
|
|
||||||
* 注意:H2 不支持 find_in_set 函数
|
|
||||||
*/
|
*/
|
||||||
H2(DbType.H2, "H2", ""),
|
H2(DbType.H2, "H2", "POSITION(',' || CAST(#{value} AS VARCHAR) || ',' IN ',' || #{column} || ',') > 0"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MySQL
|
* MySQL
|
||||||
*/
|
*/
|
||||||
MY_SQL(DbType.MYSQL, "MySQL", "FIND_IN_SET('#{value}', #{column}) <> 0"),
|
MY_SQL(DbType.MYSQL, "MySQL", "FIND_IN_SET(#{value}, #{column}) <> 0"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Oracle
|
* Oracle
|
||||||
*/
|
*/
|
||||||
ORACLE(DbType.ORACLE, "Oracle", "FIND_IN_SET('#{value}', #{column}) <> 0"),
|
ORACLE(DbType.ORACLE, "Oracle", "INSTR(',' || #{column} || ',', ',' || #{value} || ',') > 0"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PostgreSQL
|
* PostgreSQL
|
||||||
*
|
*
|
||||||
* 华为 openGauss 使用 ProductName 与 PostgreSQL 相同
|
* 华为 openGauss 使用 ProductName 与 PostgreSQL 相同
|
||||||
*/
|
*/
|
||||||
POSTGRE_SQL(DbType.POSTGRE_SQL,"PostgreSQL", "POSITION('#{value}' IN #{column}) <> 0"),
|
POSTGRE_SQL(DbType.POSTGRE_SQL, "PostgreSQL", "POSITION(',' || CAST(#{value} AS VARCHAR) || ',' IN ',' || #{column} || ',') > 0"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SQL Server
|
* SQL Server
|
||||||
*/
|
*/
|
||||||
SQL_SERVER(DbType.SQL_SERVER, "Microsoft SQL Server", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"),
|
SQL_SERVER(DbType.SQL_SERVER, "Microsoft SQL Server", "CHARINDEX(',' + CAST(#{value} AS varchar(255)) + ',', ',' + #{column} + ',') > 0"),
|
||||||
/**
|
/**
|
||||||
* SQL Server 2005
|
* SQL Server 2005
|
||||||
*/
|
*/
|
||||||
SQL_SERVER2005(DbType.SQL_SERVER2005, "Microsoft SQL Server 2005", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"),
|
SQL_SERVER2005(DbType.SQL_SERVER2005, "Microsoft SQL Server 2005", "CHARINDEX(',' + CAST(#{value} AS varchar(255)) + ',', ',' + #{column} + ',') > 0"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 达梦
|
* 达梦
|
||||||
*/
|
*/
|
||||||
DM(DbType.DM, "DM DBMS", "FIND_IN_SET('#{value}', #{column}) <> 0"),
|
DM(DbType.DM, "DM DBMS", "FIND_IN_SET(#{value}, #{column}) <> 0"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 人大金仓
|
* 人大金仓
|
||||||
*/
|
*/
|
||||||
KINGBASE_ES(DbType.KINGBASE_ES, "KingbaseES", "POSITION('#{value}' IN #{column}) <> 0"),
|
KINGBASE_ES(DbType.KINGBASE_ES, "KingbaseES", "POSITION(',' || CAST(#{value} AS VARCHAR) || ',' IN ',' || #{column} || ',') > 0"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OceanBase
|
* OceanBase
|
||||||
*/
|
*/
|
||||||
OCEAN_BASE(DbType.OCEAN_BASE, "OceanBase", "FIND_IN_SET('#{value}', #{column}) <> 0")
|
OCEAN_BASE(DbType.OCEAN_BASE, "OceanBase", "FIND_IN_SET(#{value}, #{column}) <> 0")
|
||||||
|
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
@ -95,7 +93,9 @@ public enum DbTypeEnum {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getFindInSetTemplate(DbType dbType) {
|
public static String getFindInSetTemplate(DbType dbType) {
|
||||||
return Optional.of(MAP_BY_MP.get(dbType).getFindInSetTemplate())
|
return Optional.ofNullable(MAP_BY_MP.get(dbType))
|
||||||
|
.map(DbTypeEnum::getFindInSetTemplate)
|
||||||
|
.filter(StrUtil::isNotBlank)
|
||||||
.orElseThrow(() -> new IllegalArgumentException("FIND_IN_SET not supported"));
|
.orElseThrow(() -> new IllegalArgumentException("FIND_IN_SET not supported"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,31 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
|
||||||
return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2).eq(field3, value3));
|
return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2).eq(field3, value3));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得满足条件的一条记录,并使用 FOR UPDATE 锁定。
|
||||||
|
*
|
||||||
|
* 注意:需要在事务中调用,否则锁会立即释放。
|
||||||
|
*
|
||||||
|
* @param queryWrapper 查询条件
|
||||||
|
* @return 实体
|
||||||
|
*/
|
||||||
|
default T selectOneForUpdate(LambdaQueryWrapper<T> queryWrapper) {
|
||||||
|
return selectOne(queryWrapper.last("FOR UPDATE"));
|
||||||
|
}
|
||||||
|
|
||||||
|
default T selectOneForUpdate(SFunction<T, ?> field, Object value) {
|
||||||
|
return selectOneForUpdate(new LambdaQueryWrapper<T>().eq(field, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
default T selectOneForUpdate(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2) {
|
||||||
|
return selectOneForUpdate(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2));
|
||||||
|
}
|
||||||
|
|
||||||
|
default T selectOneForUpdate(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2,
|
||||||
|
SFunction<T, ?> field3, Object value3) {
|
||||||
|
return selectOneForUpdate(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2).eq(field3, value3));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取满足条件的第 1 条记录
|
* 获取满足条件的第 1 条记录
|
||||||
*
|
*
|
||||||
|
|
@ -145,6 +170,17 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
|
||||||
return CollUtil.getFirst(list);
|
return CollUtil.getFirst(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取满足条件的最新一条记录
|
||||||
|
* <p>
|
||||||
|
* 目的:解决并发场景下,插入多条记录后,使用 selectOne 会报错的问题
|
||||||
|
*
|
||||||
|
* @param queryWrapper 查询条件
|
||||||
|
* @return 最新一条;不存在返回 null
|
||||||
|
*/
|
||||||
|
default T selectLastOne(LambdaQueryWrapper<T> queryWrapper) {
|
||||||
|
return CollUtil.getLast(selectList(queryWrapper));
|
||||||
|
}
|
||||||
|
|
||||||
default Long selectCount() {
|
default Long selectCount() {
|
||||||
return selectCount(new QueryWrapper<>());
|
return selectCount(new QueryWrapper<>());
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,12 @@ public class LambdaQueryWrapperX<T> extends LambdaQueryWrapper<T> {
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
public LambdaQueryWrapperX<T> likeRightIfPresent(SFunction<T, ?> column, String val) {
|
||||||
|
if (StringUtils.hasText(val)) {
|
||||||
|
return (LambdaQueryWrapperX<T>) super.likeRight(column, val);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public LambdaQueryWrapperX<T> inIfPresent(SFunction<T, ?> column, Collection<?> values) {
|
public LambdaQueryWrapperX<T> inIfPresent(SFunction<T, ?> column, Collection<?> values) {
|
||||||
if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
|
if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,13 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public <S> MPJLambdaWrapperX<T> likeRightIfPresent(SFunction<S, ?> column, String val) {
|
||||||
|
if (StringUtils.hasText(val)) {
|
||||||
|
return (MPJLambdaWrapperX<T>) super.likeRight(column, val);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public <S> MPJLambdaWrapperX<T> inIfPresent(SFunction<S, ?> column, Collection<?> values) {
|
public <S> MPJLambdaWrapperX<T> inIfPresent(SFunction<S, ?> column, Collection<?> values) {
|
||||||
if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
|
if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
|
||||||
return (MPJLambdaWrapperX<T>) super.in(column, values);
|
return (MPJLambdaWrapperX<T>) super.in(column, values);
|
||||||
|
|
@ -102,7 +109,6 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ========== 重写父类方法,方便链式调用 ==========
|
// ========== 重写父类方法,方便链式调用 ==========
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,13 @@ public class QueryWrapperX<T> extends QueryWrapper<T> {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public QueryWrapperX<T> likeRightIfPresent(String column, String val) {
|
||||||
|
if (StringUtils.hasText(val)) {
|
||||||
|
return (QueryWrapperX<T>) super.likeRight(column, val);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public QueryWrapperX<T> inIfPresent(String column, Collection<?> values) {
|
public QueryWrapperX<T> inIfPresent(String column, Collection<?> values) {
|
||||||
if (!CollectionUtils.isEmpty(values)) {
|
if (!CollectionUtils.isEmpty(values)) {
|
||||||
return (QueryWrapperX<T>) super.in(column, values);
|
return (QueryWrapperX<T>) super.in(column, values);
|
||||||
|
|
@ -95,13 +102,13 @@ public class QueryWrapperX<T> extends QueryWrapper<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public QueryWrapperX<T> betweenIfPresent(String column, Object[] values) {
|
public QueryWrapperX<T> betweenIfPresent(String column, Object[] values) {
|
||||||
if (values!= null && values.length != 0 && values[0] != null && values[1] != null) {
|
if (values != null && values.length != 0 && values[0] != null && values[1] != null) {
|
||||||
return (QueryWrapperX<T>) super.between(column, values[0], values[1]);
|
return (QueryWrapperX<T>) super.between(column, values[0], values[1]);
|
||||||
}
|
}
|
||||||
if (values!= null && values.length != 0 && values[0] != null) {
|
if (values != null && values.length != 0 && values[0] != null) {
|
||||||
return (QueryWrapperX<T>) ge(column, values[0]);
|
return (QueryWrapperX<T>) ge(column, values[0]);
|
||||||
}
|
}
|
||||||
if (values!= null && values.length != 0 && values[1] != null) {
|
if (values != null && values.length != 0 && values[1] != null) {
|
||||||
return (QueryWrapperX<T>) le(column, values[1]);
|
return (QueryWrapperX<T>) le(column, values[1]);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import net.sf.jsqlparser.schema.Table;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MyBatis 工具类
|
* MyBatis 工具类
|
||||||
|
|
@ -31,6 +32,12 @@ public class MyBatisUtils {
|
||||||
|
|
||||||
private static final String MYSQL_ESCAPE_CHARACTER = "`";
|
private static final String MYSQL_ESCAPE_CHARACTER = "`";
|
||||||
|
|
||||||
|
private static final Pattern SAFE_COLUMN_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]+(\\.[a-zA-Z0-9_]+)*$");
|
||||||
|
|
||||||
|
private static final String FIND_IN_SET_VALUE_PLACEHOLDER = "#{value}";
|
||||||
|
|
||||||
|
private static final String FIND_IN_SET_COLUMN_PLACEHOLDER = "#{column}";
|
||||||
|
|
||||||
public static <T> Page<T> buildPage(PageParam pageParam) {
|
public static <T> Page<T> buildPage(PageParam pageParam) {
|
||||||
return buildPage(pageParam, null);
|
return buildPage(pageParam, null);
|
||||||
}
|
}
|
||||||
|
|
@ -42,8 +49,11 @@ public class MyBatisUtils {
|
||||||
// 排序字段
|
// 排序字段
|
||||||
if (CollUtil.isNotEmpty(sortingFields)) {
|
if (CollUtil.isNotEmpty(sortingFields)) {
|
||||||
for (SortingField sortingField : sortingFields) {
|
for (SortingField sortingField : sortingFields) {
|
||||||
page.addOrder(new OrderItem().setAsc(SortingField.ORDER_ASC.equals(sortingField.getOrder()))
|
String columnName = buildSafeOrderColumn(sortingField.getField());
|
||||||
.setColumn(StrUtil.toUnderlineCase(sortingField.getField())));
|
if (columnName == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
page.addOrder(new OrderItem().setAsc(isAscOrder(sortingField.getOrder())).setColumn(columnName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return page;
|
return page;
|
||||||
|
|
@ -57,23 +67,29 @@ public class MyBatisUtils {
|
||||||
if (wrapper instanceof QueryWrapper) {
|
if (wrapper instanceof QueryWrapper) {
|
||||||
QueryWrapper<T> query = (QueryWrapper<T>) wrapper;
|
QueryWrapper<T> query = (QueryWrapper<T>) wrapper;
|
||||||
for (SortingField sortingField : sortingFields) {
|
for (SortingField sortingField : sortingFields) {
|
||||||
query.orderBy(true,
|
String columnName = buildSafeOrderColumn(sortingField.getField());
|
||||||
SortingField.ORDER_ASC.equals(sortingField.getOrder()),
|
if (columnName == null) {
|
||||||
StrUtil.toUnderlineCase(sortingField.getField()));
|
continue;
|
||||||
|
}
|
||||||
|
query.orderBy(true, isAscOrder(sortingField.getOrder()), columnName);
|
||||||
}
|
}
|
||||||
} else if (wrapper instanceof LambdaQueryWrapper) {
|
} else if (wrapper instanceof LambdaQueryWrapper) {
|
||||||
// LambdaQueryWrapper 不直接支持字符串字段排序,使用 last 方法拼接 ORDER BY
|
// LambdaQueryWrapper 不直接支持字符串字段排序,使用 last 方法拼接 ORDER BY
|
||||||
LambdaQueryWrapper<T> lambdaQuery = (LambdaQueryWrapper<T>) wrapper;
|
LambdaQueryWrapper<T> lambdaQuery = (LambdaQueryWrapper<T>) wrapper;
|
||||||
StringBuilder orderBy = new StringBuilder();
|
StringBuilder orderBy = new StringBuilder();
|
||||||
for (SortingField sortingField : sortingFields) {
|
for (SortingField sortingField : sortingFields) {
|
||||||
|
String columnName = buildSafeOrderColumn(sortingField.getField());
|
||||||
|
if (columnName == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (StrUtil.isNotEmpty(orderBy)) {
|
if (StrUtil.isNotEmpty(orderBy)) {
|
||||||
orderBy.append(", ");
|
orderBy.append(", ");
|
||||||
}
|
}
|
||||||
orderBy.append(StrUtil.toUnderlineCase(sortingField.getField()))
|
orderBy.append(columnName).append(" ").append(getOrderDirection(sortingField.getOrder()));
|
||||||
.append(" ")
|
}
|
||||||
.append(SortingField.ORDER_ASC.equals(sortingField.getOrder()) ? "ASC" : "DESC");
|
if (StrUtil.isNotEmpty(orderBy)) {
|
||||||
|
lambdaQuery.last("ORDER BY " + orderBy);
|
||||||
}
|
}
|
||||||
lambdaQuery.last("ORDER BY " + orderBy);
|
|
||||||
// 另外个思路:https://blog.csdn.net/m0_59084856/article/details/138450913
|
// 另外个思路:https://blog.csdn.net/m0_59084856/article/details/138450913
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("Unsupported wrapper type: " + wrapper.getClass().getName());
|
throw new IllegalArgumentException("Unsupported wrapper type: " + wrapper.getClass().getName());
|
||||||
|
|
@ -81,6 +97,22 @@ public class MyBatisUtils {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isAscOrder(String order) {
|
||||||
|
return SortingField.ORDER_ASC.equals(order);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getOrderDirection(String order) {
|
||||||
|
return isAscOrder(order) ? "ASC" : "DESC";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildSafeOrderColumn(String field) {
|
||||||
|
String columnName = StrUtil.toUnderlineCase(field);
|
||||||
|
if (StrUtil.isEmpty(columnName) || !SAFE_COLUMN_NAME_PATTERN.matcher(columnName).matches()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return columnName;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将拦截器添加到链中
|
* 将拦截器添加到链中
|
||||||
* 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置
|
* 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置
|
||||||
|
|
@ -129,15 +161,43 @@ public class MyBatisUtils {
|
||||||
/**
|
/**
|
||||||
* 跨数据库的 find_in_set 实现
|
* 跨数据库的 find_in_set 实现
|
||||||
*
|
*
|
||||||
* @param column 字段名称
|
* @param columnName 字段名称
|
||||||
* @param value 查询值(不带单引号)
|
|
||||||
* @return sql
|
* @return sql
|
||||||
*/
|
*/
|
||||||
public static String findInSet(String column, Object value) {
|
public static String findInSet(String columnName) {
|
||||||
|
return findInSet(columnName, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跨数据库的 find_in_set 实现,适用于同一个 apply 语句中有多个参数的场景
|
||||||
|
*
|
||||||
|
* @param columnName 字段名称
|
||||||
|
* @param paramIndex apply 参数序号
|
||||||
|
* @return sql
|
||||||
|
*/
|
||||||
|
public static String findInSetWithParamIndex(String columnName, int paramIndex) {
|
||||||
|
return findInSet(columnName, paramIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String findInSet(String columnName, int paramIndex) {
|
||||||
DbType dbType = JdbcUtils.getDbType();
|
DbType dbType = JdbcUtils.getDbType();
|
||||||
|
return findInSet(dbType, columnName, paramIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String findInSet(DbType dbType, String columnName, int paramIndex) {
|
||||||
|
if (!isSafeColumnName(columnName)) {
|
||||||
|
throw new IllegalArgumentException("Invalid column name: " + columnName);
|
||||||
|
}
|
||||||
|
if (paramIndex < 0) {
|
||||||
|
throw new IllegalArgumentException("Invalid param index: " + paramIndex);
|
||||||
|
}
|
||||||
return DbTypeEnum.getFindInSetTemplate(dbType)
|
return DbTypeEnum.getFindInSetTemplate(dbType)
|
||||||
.replace("#{column}", column)
|
.replace(FIND_IN_SET_COLUMN_PLACEHOLDER, columnName)
|
||||||
.replace("#{value}", StrUtil.toString(value));
|
.replace(FIND_IN_SET_VALUE_PLACEHOLDER, "{" + paramIndex + "}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isSafeColumnName(String columnName) {
|
||||||
|
return StrUtil.isNotEmpty(columnName) && SAFE_COLUMN_NAME_PATTERN.matcher(columnName).matches();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
package cn.iocoder.yudao.framework.mybatis.core.util;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||||
|
import cn.iocoder.yudao.framework.common.pojo.SortingField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.DbType;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link MyBatisUtils} 的单元测试
|
||||||
|
*/
|
||||||
|
public class MyBatisUtilsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBuildPage_sortingFields() {
|
||||||
|
// 准备参数
|
||||||
|
PageParam pageParam = new PageParam();
|
||||||
|
pageParam.setPageNo(2);
|
||||||
|
pageParam.setPageSize(20);
|
||||||
|
List<SortingField> sortingFields = Arrays.asList(
|
||||||
|
new SortingField("userName", SortingField.ORDER_ASC),
|
||||||
|
new SortingField("u.id", SortingField.ORDER_DESC),
|
||||||
|
new SortingField("name desc", SortingField.ORDER_DESC));
|
||||||
|
|
||||||
|
// 调用
|
||||||
|
Page<Object> page = MyBatisUtils.buildPage(pageParam, sortingFields);
|
||||||
|
|
||||||
|
// 断言
|
||||||
|
assertEquals(2, page.getCurrent());
|
||||||
|
assertEquals(20, page.getSize());
|
||||||
|
assertEquals(2, page.orders().size());
|
||||||
|
assertOrderItem(page.orders().get(0), "user_name", true);
|
||||||
|
assertOrderItem(page.orders().get(1), "u.id", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAddOrder_queryWrapper() {
|
||||||
|
// 准备参数
|
||||||
|
QueryWrapper<Object> query = new QueryWrapper<>();
|
||||||
|
List<SortingField> sortingFields = Arrays.asList(
|
||||||
|
new SortingField("userName", SortingField.ORDER_ASC),
|
||||||
|
new SortingField("u.id", SortingField.ORDER_DESC),
|
||||||
|
new SortingField("name;drop", SortingField.ORDER_ASC));
|
||||||
|
|
||||||
|
// 调用
|
||||||
|
MyBatisUtils.addOrder(query, sortingFields);
|
||||||
|
|
||||||
|
// 断言
|
||||||
|
assertEquals(" ORDER BY user_name ASC,u.id DESC", query.getSqlSegment());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAddOrder_lambdaQueryWrapper() {
|
||||||
|
// 准备参数
|
||||||
|
LambdaQueryWrapper<Object> query = new LambdaQueryWrapper<>();
|
||||||
|
List<SortingField> sortingFields = Arrays.asList(
|
||||||
|
new SortingField("userName", SortingField.ORDER_ASC),
|
||||||
|
new SortingField("u.id", SortingField.ORDER_DESC),
|
||||||
|
new SortingField("name`", SortingField.ORDER_ASC));
|
||||||
|
|
||||||
|
// 调用
|
||||||
|
MyBatisUtils.addOrder(query, sortingFields);
|
||||||
|
|
||||||
|
// 断言
|
||||||
|
assertEquals(" ORDER BY user_name ASC, u.id DESC", query.getSqlSegment());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAddOrder_lambdaQueryWrapper_invalidSortingFields() {
|
||||||
|
// 准备参数
|
||||||
|
LambdaQueryWrapper<Object> query = new LambdaQueryWrapper<>();
|
||||||
|
List<SortingField> sortingFields = Arrays.asList(
|
||||||
|
new SortingField("name desc", SortingField.ORDER_ASC),
|
||||||
|
new SortingField("name;drop", SortingField.ORDER_DESC));
|
||||||
|
|
||||||
|
// 调用
|
||||||
|
MyBatisUtils.addOrder(query, sortingFields);
|
||||||
|
|
||||||
|
// 断言
|
||||||
|
assertEquals("", query.getSqlSegment());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOrderDirection() {
|
||||||
|
assertTrue(MyBatisUtils.isAscOrder(SortingField.ORDER_ASC));
|
||||||
|
assertFalse(MyBatisUtils.isAscOrder(SortingField.ORDER_DESC));
|
||||||
|
assertEquals("ASC", MyBatisUtils.getOrderDirection(SortingField.ORDER_ASC));
|
||||||
|
assertEquals("DESC", MyBatisUtils.getOrderDirection(SortingField.ORDER_DESC));
|
||||||
|
assertEquals("DESC", MyBatisUtils.getOrderDirection(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFindInSet() {
|
||||||
|
assertEquals("FIND_IN_SET({0}, websites) <> 0",
|
||||||
|
MyBatisUtils.findInSet(DbType.MYSQL, "websites", 0));
|
||||||
|
assertEquals("POSITION(',' || CAST({0} AS VARCHAR) || ',' IN ',' || websites || ',') > 0",
|
||||||
|
MyBatisUtils.findInSet(DbType.H2, "websites", 0));
|
||||||
|
assertEquals("INSTR(',' || t.websites || ',', ',' || {0} || ',') > 0",
|
||||||
|
MyBatisUtils.findInSet(DbType.ORACLE, "t.websites", 0));
|
||||||
|
assertEquals("POSITION(',' || CAST({1} AS VARCHAR) || ',' IN ',' || websites || ',') > 0",
|
||||||
|
MyBatisUtils.findInSet(DbType.POSTGRE_SQL, "websites", 1));
|
||||||
|
assertEquals("CHARINDEX(',' + CAST({2} AS varchar(255)) + ',', ',' + websites + ',') > 0",
|
||||||
|
MyBatisUtils.findInSet(DbType.SQL_SERVER, "websites", 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFindInSet_invalidColumnName() {
|
||||||
|
assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> MyBatisUtils.findInSet(DbType.MYSQL, "websites;drop table system_tenant", 0));
|
||||||
|
assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> MyBatisUtils.findInSet(DbType.MYSQL, "FIND_IN_SET(value, websites)", 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFindInSet_invalidParamIndex() {
|
||||||
|
assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> MyBatisUtils.findInSet(DbType.MYSQL, "websites", -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFindInSet_applyBindsValue() {
|
||||||
|
// 准备参数
|
||||||
|
QueryWrapper<Object> query = new QueryWrapper<>();
|
||||||
|
String value = "test' OR 1 = 1";
|
||||||
|
|
||||||
|
// 调用
|
||||||
|
query.apply(MyBatisUtils.findInSet(DbType.MYSQL, "to_mails", 0), value);
|
||||||
|
|
||||||
|
// 断言:SQL 片段里只有 MyBatis Plus 参数占位,用户输入不会被直接拼接进去
|
||||||
|
assertEquals("(FIND_IN_SET(#{ew.paramNameValuePairs.MPGENVAL1}, to_mails) <> 0)",
|
||||||
|
query.getSqlSegment());
|
||||||
|
assertFalse(query.getSqlSegment().contains(value));
|
||||||
|
assertEquals(value, query.getParamNameValuePairs().get("MPGENVAL1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFindInSet_applyBindsMultipleValues() {
|
||||||
|
// 准备参数
|
||||||
|
QueryWrapper<Object> query = new QueryWrapper<>();
|
||||||
|
String value1 = "1' OR 1 = 1";
|
||||||
|
String value2 = "2' OR 1 = 1";
|
||||||
|
|
||||||
|
// 调用
|
||||||
|
query.apply(MyBatisUtils.findInSet(DbType.MYSQL, "tag_ids", 0)
|
||||||
|
+ " OR " + MyBatisUtils.findInSet(DbType.MYSQL, "tag_ids", 1), value1, value2);
|
||||||
|
|
||||||
|
// 断言:多个参数都由 MyBatis Plus 生成占位符,不拼接用户输入
|
||||||
|
assertEquals("(FIND_IN_SET(#{ew.paramNameValuePairs.MPGENVAL1}, tag_ids) <> 0"
|
||||||
|
+ " OR FIND_IN_SET(#{ew.paramNameValuePairs.MPGENVAL2}, tag_ids) <> 0)",
|
||||||
|
query.getSqlSegment());
|
||||||
|
assertFalse(query.getSqlSegment().contains(value1));
|
||||||
|
assertFalse(query.getSqlSegment().contains(value2));
|
||||||
|
assertEquals(value1, query.getParamNameValuePairs().get("MPGENVAL1"));
|
||||||
|
assertEquals(value2, query.getParamNameValuePairs().get("MPGENVAL2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertOrderItem(OrderItem orderItem, String column, boolean asc) {
|
||||||
|
assertEquals(column, orderItem.getColumn());
|
||||||
|
assertEquals(asc, orderItem.isAsc());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -75,8 +75,12 @@ public class YudaoCacheAutoConfiguration {
|
||||||
RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory());
|
RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory());
|
||||||
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory,
|
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory,
|
||||||
BatchStrategies.scan(yudaoCacheProperties.getRedisScanBatchSize()));
|
BatchStrategies.scan(yudaoCacheProperties.getRedisScanBatchSize()));
|
||||||
// 创建 TenantRedisCacheManager 对象
|
// 创建 TimeoutRedisCacheManager 对象
|
||||||
return new TimeoutRedisCacheManager(cacheWriter, redisCacheConfiguration);
|
TimeoutRedisCacheManager cacheManager = new TimeoutRedisCacheManager(cacheWriter, redisCacheConfiguration);
|
||||||
|
// 开启事务感知:@Transactional 方法内的 @CacheEvict / @CachePut 自动延迟到 afterCommit,
|
||||||
|
// 避免事务未提交就清缓存被并发读穿写脏值;无事务时立即生效,行为不变
|
||||||
|
cacheManager.setTransactionAware(true);
|
||||||
|
return cacheManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,8 @@
|
||||||
|
|
||||||
<!-- 工具相关 -->
|
<!-- 工具相关 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>jakarta.validation</groupId>
|
<groupId>javax.validation</groupId>
|
||||||
<artifactId>jakarta.validation-api</artifactId>
|
<artifactId>validation-api</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ public class ApiAccessLogFilter extends ApiRequestFilter {
|
||||||
LocalDateTime beginTime = LocalDateTime.now();
|
LocalDateTime beginTime = LocalDateTime.now();
|
||||||
// 提前获得参数,避免 XssFilter 过滤处理
|
// 提前获得参数,避免 XssFilter 过滤处理
|
||||||
Map<String, String> queryString = ServletUtils.getParamMap(request);
|
Map<String, String> queryString = ServletUtils.getParamMap(request);
|
||||||
String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null;
|
String requestBody = ServletUtils.getBody(request);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 继续过滤器
|
// 继续过滤器
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ public class ApiAccessLogInterceptor implements HandlerInterceptor {
|
||||||
// 打印 request 日志
|
// 打印 request 日志
|
||||||
if (!SpringUtils.isProd()) {
|
if (!SpringUtils.isProd()) {
|
||||||
Map<String, String> queryString = ServletUtils.getParamMap(request);
|
Map<String, String> queryString = ServletUtils.getParamMap(request);
|
||||||
String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null;
|
String requestBody = ServletUtils.getBody(request);
|
||||||
if (CollUtil.isEmpty(queryString) && StrUtil.isEmpty(requestBody)) {
|
if (CollUtil.isEmpty(queryString) && StrUtil.isEmpty(requestBody)) {
|
||||||
log.info("[preHandle][开始请求 URL({}) 无参数]", request.getRequestURI());
|
log.info("[preHandle][开始请求 URL({}) 无参数]", request.getRequestURI());
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,14 @@ public class BannerApplicationRunner implements ApplicationRunner {
|
||||||
System.out.println("[商城系统 yudao-module-mall 教程][参考 https://cloud.iocoder.cn/mall/build/ 开启]");
|
System.out.println("[商城系统 yudao-module-mall 教程][参考 https://cloud.iocoder.cn/mall/build/ 开启]");
|
||||||
// ERP 系统
|
// ERP 系统
|
||||||
System.out.println("[ERP 系统 yudao-module-erp - 教程][参考 https://cloud.iocoder.cn/erp/build/ 开启]");
|
System.out.println("[ERP 系统 yudao-module-erp - 教程][参考 https://cloud.iocoder.cn/erp/build/ 开启]");
|
||||||
|
// WMS 仓库管理系统
|
||||||
|
System.out.println("[WMS 仓库管理系统 yudao-module-wms - 教程][参考 https://cloud.iocoder.cn/wms/build/ 开启]");
|
||||||
// CRM 系统
|
// CRM 系统
|
||||||
System.out.println("[CRM 系统 yudao-module-crm - 教程][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
|
System.out.println("[CRM 系统 yudao-module-crm - 教程][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
|
||||||
|
// MES 系统
|
||||||
|
System.out.println("[MES 系统 yudao-module-mes - 教程][参考 https://cloud.iocoder.cn/mes/build/ 开启]");
|
||||||
|
// IM 即时通讯
|
||||||
|
System.out.println("[IM 即时通讯 yudao-module-im - 教程][参考 https://cloud.iocoder.cn/im/build/ 开启]");
|
||||||
// 微信公众号
|
// 微信公众号
|
||||||
System.out.println("[微信公众号 yudao-module-mp 教程][参考 https://cloud.iocoder.cn/mp/build/ 开启]");
|
System.out.println("[微信公众号 yudao-module-mp 教程][参考 https://cloud.iocoder.cn/mp/build/ 开启]");
|
||||||
// 支付平台
|
// 支付平台
|
||||||
|
|
|
||||||
|
|
@ -410,25 +410,43 @@ public class GlobalExceptionHandler {
|
||||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||||
"[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]");
|
"[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]");
|
||||||
}
|
}
|
||||||
// 6. CRM 系统
|
// 6. WMS 仓库管理系统
|
||||||
|
if (message.contains("wms_")) {
|
||||||
|
log.error("[WMS 仓库管理系统 yudao-module-wms - 表结构未导入][参考 https://cloud.iocoder.cn/wms/build/ 开启]");
|
||||||
|
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||||
|
"[WMS 仓库管理系统 yudao-module-wms - 表结构未导入][参考 https://cloud.iocoder.cn/wms/build/ 开启]");
|
||||||
|
}
|
||||||
|
// 7. CRM 系统
|
||||||
if (message.contains("crm_")) {
|
if (message.contains("crm_")) {
|
||||||
log.error("[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
|
log.error("[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
|
||||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||||
"[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
|
"[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
|
||||||
}
|
}
|
||||||
// 7. 支付平台
|
// 8. MES 系统
|
||||||
|
if (message.contains("mes_")) {
|
||||||
|
log.error("[MES 系统 yudao-module-mes - 表结构未导入][参考 https://cloud.iocoder.cn/mes/build/ 开启]");
|
||||||
|
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||||
|
"[MES 系统 yudao-module-mes - 表结构未导入][参考 https://cloud.iocoder.cn/mes/build/ 开启]");
|
||||||
|
}
|
||||||
|
// 9. IM 即时通讯
|
||||||
|
if (message.contains("im_")) {
|
||||||
|
log.error("[IM 即时通讯 yudao-module-im - 表结构未导入][参考 https://cloud.iocoder.cn/im/build/ 开启]");
|
||||||
|
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||||
|
"[IM 即时通讯 yudao-module-im - 表结构未导入][参考 https://cloud.iocoder.cn/im/build/ 开启]");
|
||||||
|
}
|
||||||
|
// 10. 支付平台
|
||||||
if (message.contains("pay_")) {
|
if (message.contains("pay_")) {
|
||||||
log.error("[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]");
|
log.error("[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]");
|
||||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||||
"[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]");
|
"[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]");
|
||||||
}
|
}
|
||||||
// 8. AI 大模型
|
// 11. AI 大模型
|
||||||
if (message.contains("ai_")) {
|
if (message.contains("ai_")) {
|
||||||
log.error("[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
|
log.error("[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
|
||||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||||
"[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
|
"[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
|
||||||
}
|
}
|
||||||
// 9. IoT 物联网
|
// 12. IoT 物联网
|
||||||
if (message.contains("iot_")) {
|
if (message.contains("iot_")) {
|
||||||
log.error("[IoT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]");
|
log.error("[IoT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]");
|
||||||
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ public class JsonWebSocketMessageHandler extends TextWebSocketHandler {
|
||||||
Long tenantId = WebSocketFrameworkUtils.getTenantId(session);
|
Long tenantId = WebSocketFrameworkUtils.getTenantId(session);
|
||||||
TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, messageObj));
|
TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, messageObj));
|
||||||
} catch (Throwable ex) {
|
} catch (Throwable ex) {
|
||||||
log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload());
|
log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload(), ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,10 @@ public class LoginUserHandshakeInterceptor implements HandshakeInterceptor {
|
||||||
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
|
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
|
||||||
WebSocketHandler wsHandler, Map<String, Object> attributes) {
|
WebSocketHandler wsHandler, Map<String, Object> attributes) {
|
||||||
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
|
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
|
||||||
if (loginUser != null) {
|
if (loginUser == null) {
|
||||||
WebSocketFrameworkUtils.setLoginUser(loginUser, attributes);
|
return false;
|
||||||
}
|
}
|
||||||
|
WebSocketFrameworkUtils.setLoginUser(loginUser, attributes);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,11 +40,6 @@
|
||||||
<artifactId>javax.servlet-api</artifactId>
|
<artifactId>javax.servlet-api</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>javax.servlet</groupId>
|
|
||||||
<artifactId>javax.servlet-api</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.xiaoymin</groupId> <!-- 接口文档 -->
|
<groupId>com.github.xiaoymin</groupId> <!-- 接口文档 -->
|
||||||
<artifactId>knife4j-gateway-spring-boot-starter</artifactId>
|
<artifactId>knife4j-gateway-spring-boot-starter</artifactId>
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,14 @@ public class BannerApplicationRunner implements ApplicationRunner {
|
||||||
System.out.println("[商城系统 yudao-module-mall 教程][参考 https://cloud.iocoder.cn/mall/build/ 开启]");
|
System.out.println("[商城系统 yudao-module-mall 教程][参考 https://cloud.iocoder.cn/mall/build/ 开启]");
|
||||||
// ERP 系统
|
// ERP 系统
|
||||||
System.out.println("[ERP 系统 yudao-module-erp - 教程][参考 https://cloud.iocoder.cn/erp/build/ 开启]");
|
System.out.println("[ERP 系统 yudao-module-erp - 教程][参考 https://cloud.iocoder.cn/erp/build/ 开启]");
|
||||||
|
// WMS 仓库管理系统
|
||||||
|
System.out.println("[WMS 仓库管理系统 yudao-module-wms - 教程][参考 https://cloud.iocoder.cn/wms/build/ 开启]");
|
||||||
// CRM 系统
|
// CRM 系统
|
||||||
System.out.println("[CRM 系统 yudao-module-crm - 教程][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
|
System.out.println("[CRM 系统 yudao-module-crm - 教程][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
|
||||||
|
// MES 系统
|
||||||
|
System.out.println("[MES 系统 yudao-module-mes - 教程][参考 https://cloud.iocoder.cn/mes/build/ 开启]");
|
||||||
|
// IM 即时通讯
|
||||||
|
System.out.println("[IM 即时通讯 yudao-module-im - 教程][参考 https://cloud.iocoder.cn/im/build/ 开启]");
|
||||||
// 微信公众号
|
// 微信公众号
|
||||||
System.out.println("[微信公众号 yudao-module-mp 教程][参考 https://cloud.iocoder.cn/mp/build/ 开启]");
|
System.out.println("[微信公众号 yudao-module-mp 教程][参考 https://cloud.iocoder.cn/mp/build/ 开启]");
|
||||||
// 支付平台
|
// 支付平台
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,20 @@ spring:
|
||||||
- Path=/admin-api/mes/**
|
- Path=/admin-api/mes/**
|
||||||
filters:
|
filters:
|
||||||
- RewritePath=/admin-api/mes/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
|
- RewritePath=/admin-api/mes/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
|
||||||
|
## wms-server 服务
|
||||||
|
- id: wms-admin-api # 路由的编号
|
||||||
|
uri: grayLb://wms-server
|
||||||
|
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
|
||||||
|
- Path=/admin-api/wms/**
|
||||||
|
filters:
|
||||||
|
- RewritePath=/admin-api/wms/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
|
||||||
|
## im-server 服务
|
||||||
|
- id: im-admin-api # 路由的编号
|
||||||
|
uri: grayLb://im-server
|
||||||
|
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
|
||||||
|
- Path=/admin-api/im/**
|
||||||
|
filters:
|
||||||
|
- RewritePath=/admin-api/im/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
|
||||||
x-forwarded:
|
x-forwarded:
|
||||||
prefix-enabled: false # 避免 Swagger 重复带上额外的 /admin-api/system 前缀
|
prefix-enabled: false # 避免 Swagger 重复带上额外的 /admin-api/system 前缀
|
||||||
default-filters: # 全局过滤器,对应 GatewayFilterDefinition 数组
|
default-filters: # 全局过滤器,对应 GatewayFilterDefinition 数组
|
||||||
|
|
@ -261,6 +275,12 @@ knife4j:
|
||||||
- name: mes-server
|
- name: mes-server
|
||||||
service-name: mes-server
|
service-name: mes-server
|
||||||
url: /admin-api/mes/v3/api-docs
|
url: /admin-api/mes/v3/api-docs
|
||||||
|
- name: wms-server
|
||||||
|
service-name: wms-server
|
||||||
|
url: /admin-api/wms/v3/api-docs
|
||||||
|
- name: im-server
|
||||||
|
service-name: im-server
|
||||||
|
url: /admin-api/im/v3/api-docs
|
||||||
|
|
||||||
--- #################### 芋道相关配置 ####################
|
--- #################### 芋道相关配置 ####################
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ spring:
|
||||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
|
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
|
||||||
# url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # MySQL Connector/J 5.X 连接的示例
|
# url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # MySQL Connector/J 5.X 连接的示例
|
||||||
# url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例
|
# url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例
|
||||||
|
# url: jdbc:postgresql://127.0.0.1:5866/ruoyi-vue-pro # 瀚高 HighGo 连接的示例
|
||||||
# url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
|
# url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
|
||||||
# url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro # SQLServer 连接的示例
|
# url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro # SQLServer 连接的示例
|
||||||
# url: jdbc:dm://10.211.55.4:5236?schema=RUOYI_VUE_PRO # DM 连接的示例
|
# url: jdbc:dm://10.211.55.4:5236?schema=RUOYI_VUE_PRO # DM 连接的示例
|
||||||
|
|
@ -71,6 +72,8 @@ spring:
|
||||||
# password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W # SQL Server 连接的示例
|
# password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W # SQL Server 连接的示例
|
||||||
# username: SYSDBA # DM 连接的示例
|
# username: SYSDBA # DM 连接的示例
|
||||||
# password: SYSDBA # DM 连接的示例
|
# password: SYSDBA # DM 连接的示例
|
||||||
|
# username: root # 瀚高 HighGo 连接的示例
|
||||||
|
# password: 123456 # 瀚高 HighGo 连接的示例
|
||||||
slave: # 模拟从库,可根据自己需要修改
|
slave: # 模拟从库,可根据自己需要修改
|
||||||
lazy: true # 开启懒加载,保证启动速度
|
lazy: true # 开启懒加载,保证启动速度
|
||||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package cn.iocoder.yudao.module.bpm.enums.task;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附件类型枚举
|
||||||
|
*
|
||||||
|
* @author jason
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum BpmAttachmentTypeEnum {
|
||||||
|
|
||||||
|
TASK_ATTACHMENT("1", "用户任务附件");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作类型
|
||||||
|
* <p>
|
||||||
|
* 由于 BPM attachment 类型为 String,所以这里就不使用 Integer
|
||||||
|
*/
|
||||||
|
private final String type;
|
||||||
|
/**
|
||||||
|
* 操作名字
|
||||||
|
*/
|
||||||
|
private final String name;
|
||||||
|
}
|
||||||
|
|
@ -107,6 +107,9 @@ public class BpmApprovalDetailRespVO {
|
||||||
@Schema(description = "签名", example = "https://www.iocoder.cn/sign.png")
|
@Schema(description = "签名", example = "https://www.iocoder.cn/sign.png")
|
||||||
private String signPicUrl;
|
private String signPicUrl;
|
||||||
|
|
||||||
|
@Schema(description = "附件", example = "[https://test.yudao.iocoder.cn/20260609/test.txt]")
|
||||||
|
private List<String> attachments;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@ public class BpmTaskApproveReqVO {
|
||||||
@Schema(description = "签名", example = "https://www.iocoder.cn/sign.png")
|
@Schema(description = "签名", example = "https://www.iocoder.cn/sign.png")
|
||||||
private String signPicUrl;
|
private String signPicUrl;
|
||||||
|
|
||||||
|
@Schema(description = "附件", example = "[https://test.yudao.iocoder.cn/20260609/test.txt]")
|
||||||
|
private List<String> attachments;
|
||||||
|
|
||||||
@Schema(description = "变量实例(动态表单)", requiredMode = Schema.RequiredMode.REQUIRED)
|
@Schema(description = "变量实例(动态表单)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
private Map<String, Object> variables;
|
private Map<String, Object> variables;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import javax.validation.constraints.NotEmpty;
|
import javax.validation.constraints.NotEmpty;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Schema(description = "管理后台 - 不通过流程任务的 Request VO")
|
@Schema(description = "管理后台 - 不通过流程任务的 Request VO")
|
||||||
@Data
|
@Data
|
||||||
|
|
@ -16,4 +17,7 @@ public class BpmTaskRejectReqVO {
|
||||||
@Schema(description = "审批意见", requiredMode = Schema.RequiredMode.REQUIRED, example = "不错不错!")
|
@Schema(description = "审批意见", requiredMode = Schema.RequiredMode.REQUIRED, example = "不错不错!")
|
||||||
private String reason;
|
private String reason;
|
||||||
|
|
||||||
|
@Schema(description = "附件", example = "[https://test.yudao.iocoder.cn/20260609/test.txt]")
|
||||||
|
private List<String> attachments;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
|
||||||
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
|
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
|
||||||
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
|
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
|
||||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||||
|
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
|
||||||
import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
|
import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
|
||||||
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
|
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
|
||||||
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
|
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
|
||||||
|
|
@ -21,7 +22,6 @@ import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO;
|
||||||
import cn.iocoder.yudao.module.bpm.convert.definition.BpmProcessDefinitionConvert;
|
import cn.iocoder.yudao.module.bpm.convert.definition.BpmProcessDefinitionConvert;
|
||||||
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
|
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
|
||||||
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
|
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
|
||||||
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
|
|
||||||
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
|
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
|
||||||
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
|
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
|
||||||
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
|
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
|
||||||
|
|
@ -34,6 +34,7 @@ import org.flowable.bpmn.model.BpmnModel;
|
||||||
import org.flowable.engine.history.HistoricProcessInstance;
|
import org.flowable.engine.history.HistoricProcessInstance;
|
||||||
import org.flowable.engine.repository.ProcessDefinition;
|
import org.flowable.engine.repository.ProcessDefinition;
|
||||||
import org.flowable.engine.runtime.ProcessInstance;
|
import org.flowable.engine.runtime.ProcessInstance;
|
||||||
|
import org.flowable.engine.task.Attachment;
|
||||||
import org.flowable.task.api.Task;
|
import org.flowable.task.api.Task;
|
||||||
import org.flowable.task.api.history.HistoricTaskInstance;
|
import org.flowable.task.api.history.HistoricTaskInstance;
|
||||||
import org.mapstruct.Mapper;
|
import org.mapstruct.Mapper;
|
||||||
|
|
@ -82,7 +83,7 @@ public interface BpmProcessInstanceConvert {
|
||||||
if (CollUtil.isNotEmpty(respVO.getTasks())) {
|
if (CollUtil.isNotEmpty(respVO.getTasks())) {
|
||||||
respVO.getTasks().forEach(task -> {
|
respVO.getTasks().forEach(task -> {
|
||||||
AdminUserRespDTO assigneeUser = userMap.get(task.getAssignee());
|
AdminUserRespDTO assigneeUser = userMap.get(task.getAssignee());
|
||||||
if (assigneeUser!= null) {
|
if (assigneeUser != null) {
|
||||||
task.setAssigneeUser(BeanUtils.toBean(assigneeUser, UserSimpleBaseVO.class));
|
task.setAssigneeUser(BeanUtils.toBean(assigneeUser, UserSimpleBaseVO.class));
|
||||||
MapUtils.findAndThen(deptMap, assigneeUser.getDeptId(), dept -> task.getAssigneeUser().setDeptName(dept.getName()));
|
MapUtils.findAndThen(deptMap, assigneeUser.getDeptId(), dept -> task.getAssigneeUser().setDeptName(dept.getName()));
|
||||||
}
|
}
|
||||||
|
|
@ -157,7 +158,7 @@ public interface BpmProcessInstanceConvert {
|
||||||
// 基本信息
|
// 基本信息
|
||||||
respVO.setProcessInstance(BeanUtils.toBean(processInstance, BpmProcessInstanceRespVO.class, o -> o
|
respVO.setProcessInstance(BeanUtils.toBean(processInstance, BpmProcessInstanceRespVO.class, o -> o
|
||||||
.setStatus(FlowableUtils.getProcessInstanceStatus(processInstance)))
|
.setStatus(FlowableUtils.getProcessInstanceStatus(processInstance)))
|
||||||
.setStartUser(buildUser(processInstance.getStartUserId(), userMap, deptMap)));
|
.setStartUser(buildUser(processInstance.getStartUserId(), userMap, deptMap)));
|
||||||
respVO.setTasks(convertList(taskInstances, task -> BeanUtils.toBean(task, BpmTaskRespVO.class)
|
respVO.setTasks(convertList(taskInstances, task -> BeanUtils.toBean(task, BpmTaskRespVO.class)
|
||||||
.setStatus(FlowableUtils.getTaskStatus(task)).setReason(FlowableUtils.getTaskReason(task))
|
.setStatus(FlowableUtils.getTaskStatus(task)).setReason(FlowableUtils.getTaskReason(task))
|
||||||
.setAssigneeUser(buildUser(task.getAssignee(), userMap, deptMap))
|
.setAssigneeUser(buildUser(task.getAssignee(), userMap, deptMap))
|
||||||
|
|
@ -200,13 +201,14 @@ public interface BpmProcessInstanceConvert {
|
||||||
return userVO;
|
return userVO;
|
||||||
}
|
}
|
||||||
|
|
||||||
default BpmApprovalDetailRespVO.ActivityNodeTask buildApprovalTaskInfo(HistoricTaskInstance task) {
|
default BpmApprovalDetailRespVO.ActivityNodeTask buildApprovalTaskInfo(HistoricTaskInstance task, List<Attachment> attachments) {
|
||||||
if (task == null) {
|
if (task == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return BeanUtils.toBean(task, BpmApprovalDetailRespVO.ActivityNodeTask.class)
|
return BeanUtils.toBean(task, BpmApprovalDetailRespVO.ActivityNodeTask.class)
|
||||||
.setStatus(FlowableUtils.getTaskStatus(task)).setReason(FlowableUtils.getTaskReason(task))
|
.setStatus(FlowableUtils.getTaskStatus(task)).setReason(FlowableUtils.getTaskReason(task))
|
||||||
.setSignPicUrl(FlowableUtils.getTaskSignPicUrl(task));
|
.setSignPicUrl(FlowableUtils.getTaskSignPicUrl(task))
|
||||||
|
.setAttachments(convertList(attachments, Attachment::getUrl));
|
||||||
}
|
}
|
||||||
|
|
||||||
default Set<Long> parseUserIds(HistoricProcessInstance processInstance,
|
default Set<Long> parseUserIds(HistoricProcessInstance processInstance,
|
||||||
|
|
|
||||||
|
|
@ -456,6 +456,54 @@ public class BpmnModelUtils {
|
||||||
return new ArrayList<>();
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据节点,递归获取上游 source 为 UserTask 的入口连线
|
||||||
|
*
|
||||||
|
* 1. 如果当前节点的直接入口连线 source 就是 UserTask,则直接返回该连线
|
||||||
|
* 2. 如果当前节点的直接入口连线 source 不是 UserTask,则继续向上递归查找
|
||||||
|
* 3. 如果递归过程中遇到 StartEvent 或 SubProcess,则停止该分支继续向上查找
|
||||||
|
*
|
||||||
|
* @param source 起始节点
|
||||||
|
* @return 上游连接 UserTask 的入口连线列表
|
||||||
|
*/
|
||||||
|
public static List<SequenceFlow> getElementIncomingUserTaskFlows(FlowElement source) {
|
||||||
|
List<SequenceFlow> result = new ArrayList<>();
|
||||||
|
collectElementIncomingUserTaskFlows(source, new HashSet<>(), new HashSet<>(), result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void collectElementIncomingUserTaskFlows(FlowElement source, Set<String> visitedSequenceFlowIds,
|
||||||
|
Set<String> resultSequenceFlowIds, List<SequenceFlow> result) {
|
||||||
|
// 如果是开始节点或子流程,则停止该分支向上查找
|
||||||
|
if (source == null || source instanceof StartEvent || source instanceof SubProcess) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 获取入口连线
|
||||||
|
List<SequenceFlow> incomingFlows = getElementIncomingFlows(source);
|
||||||
|
if (CollUtil.isEmpty(incomingFlows)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 循环找到目标元素
|
||||||
|
for (SequenceFlow incomingFlow : incomingFlows) {
|
||||||
|
// 如果发现连线重复,说明连线已经走过。跳过
|
||||||
|
if (incomingFlow == null || !visitedSequenceFlowIds.add(incomingFlow.getId())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 如果 source 是 UserTask,则添加到结果中
|
||||||
|
FlowElement sourceFlowElement = incomingFlow.getSourceFlowElement();
|
||||||
|
if (sourceFlowElement instanceof UserTask) {
|
||||||
|
if (resultSequenceFlowIds.add(incomingFlow.getId())) {
|
||||||
|
result.add(incomingFlow);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 递归向上查找 UserTask
|
||||||
|
collectElementIncomingUserTaskFlows(sourceFlowElement, visitedSequenceFlowIds,
|
||||||
|
resultSequenceFlowIds, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据节点,获取出口连线
|
* 根据节点,获取出口连线
|
||||||
*
|
*
|
||||||
|
|
@ -673,7 +721,7 @@ public class BpmnModelUtils {
|
||||||
// 这条线路存在目标节点,直接返回 true
|
// 这条线路存在目标节点,直接返回 true
|
||||||
FlowElement sourceFlowElement = sequenceFlow.getSourceFlowElement();
|
FlowElement sourceFlowElement = sequenceFlow.getSourceFlowElement();
|
||||||
if (target.getId().equals(sourceFlowElement.getId())) {
|
if (target.getId().equals(sourceFlowElement.getId())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// 如果目标节点为并行网关,跳过这个循环 (TODO 疑问:这个判断作用是防止回退到并行网关分支上的节点吗?)
|
// 如果目标节点为并行网关,跳过这个循环 (TODO 疑问:这个判断作用是防止回退到并行网关分支上的节点吗?)
|
||||||
if (sourceFlowElement instanceof ParallelGateway) {
|
if (sourceFlowElement instanceof ParallelGateway) {
|
||||||
|
|
@ -798,9 +846,9 @@ public class BpmnModelUtils {
|
||||||
|
|
||||||
// 情况:StartEvent/EndEvent/UserTask/ServiceTask
|
// 情况:StartEvent/EndEvent/UserTask/ServiceTask
|
||||||
if (currentElement instanceof StartEvent
|
if (currentElement instanceof StartEvent
|
||||||
|| currentElement instanceof EndEvent
|
|| currentElement instanceof EndEvent
|
||||||
|| currentElement instanceof UserTask
|
|| currentElement instanceof UserTask
|
||||||
|| currentElement instanceof ServiceTask) {
|
|| currentElement instanceof ServiceTask) {
|
||||||
// 添加节点
|
// 添加节点
|
||||||
FlowNode flowNode = (FlowNode) currentElement;
|
FlowNode flowNode = (FlowNode) currentElement;
|
||||||
resultElements.add(flowNode);
|
resultElements.add(flowNode);
|
||||||
|
|
@ -982,8 +1030,8 @@ public class BpmnModelUtils {
|
||||||
*/
|
*/
|
||||||
private static SequenceFlow findMatchSequenceFlowByExclusiveGateway(Gateway gateway, Map<String, Object> variables) {
|
private static SequenceFlow findMatchSequenceFlowByExclusiveGateway(Gateway gateway, Map<String, Object> variables) {
|
||||||
SequenceFlow matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
|
SequenceFlow matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
|
||||||
flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())
|
flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())
|
||||||
&& (evalConditionExpress(variables, flow.getConditionExpression())));
|
&& (evalConditionExpress(variables, flow.getConditionExpression())));
|
||||||
if (matchSequenceFlow == null) {
|
if (matchSequenceFlow == null) {
|
||||||
matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
|
matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
|
||||||
flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId()));
|
flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId()));
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormFi
|
||||||
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
|
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
|
||||||
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
|
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
|
||||||
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
|
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
import org.flowable.common.engine.api.delegate.Expression;
|
import org.flowable.common.engine.api.delegate.Expression;
|
||||||
import org.flowable.common.engine.api.variable.VariableContainer;
|
import org.flowable.common.engine.api.variable.VariableContainer;
|
||||||
|
|
@ -27,6 +28,7 @@ import org.flowable.engine.runtime.ProcessInstance;
|
||||||
import org.flowable.task.api.TaskInfo;
|
import org.flowable.task.api.TaskInfo;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
@ -245,10 +247,10 @@ public class FlowableUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析表单配置
|
// 解析表单配置
|
||||||
Map<String, BpmFormFieldVO> formFieldsMap = new HashMap<>();
|
Map<String, BpmFormFieldVO> formFieldsMap = new LinkedHashMap<>();
|
||||||
processDefinitionInfo.getFormFields().forEach(formFieldStr -> {
|
processDefinitionInfo.getFormFields().forEach(formFieldStr -> {
|
||||||
BpmFormFieldVO formField = JsonUtils.parseObject(formFieldStr, BpmFormFieldVO.class);
|
JsonNode formFieldNode = JsonUtils.parseObject(formFieldStr, JsonNode.class);
|
||||||
parseFormField(formField, formFieldsMap);
|
parseFormField(formFieldNode, formFieldsMap);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 情况一:当自定义了摘要
|
// 情况一:当自定义了摘要
|
||||||
|
|
@ -275,18 +277,32 @@ public class FlowableUtils {
|
||||||
/**
|
/**
|
||||||
* 递归解析表单字段
|
* 递归解析表单字段
|
||||||
*/
|
*/
|
||||||
private static void parseFormField(BpmFormFieldVO formField, Map<String, BpmFormFieldVO> formFieldsMap) {
|
private static void parseFormField(JsonNode formFieldNode, Map<String, BpmFormFieldVO> formFieldsMap) {
|
||||||
if (formField == null) {
|
if (formFieldNode == null || !formFieldNode.isObject()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 如果存在 children -> 说明是布局组件
|
|
||||||
if (formField.getChildren() != null && !formField.getChildren().isEmpty()) {
|
// 如果 children 里存在对象节点,说明是布局组件;字符串节点是分割线、标签、文字等展示组件内容,直接跳过。
|
||||||
for (BpmFormFieldVO child : formField.getChildren()) {
|
JsonNode children = formFieldNode.get("children");
|
||||||
|
if (children != null && children.isArray() && children.size() > 0) {
|
||||||
|
boolean hasObjectChild = false;
|
||||||
|
for (JsonNode child : children) {
|
||||||
|
if (!child.isObject()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
hasObjectChild = true;
|
||||||
parseFormField(child, formFieldsMap);
|
parseFormField(child, formFieldsMap);
|
||||||
}
|
}
|
||||||
return;
|
if (hasObjectChild) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 真实字段才加入 map
|
// 真实字段才加入 map
|
||||||
|
BpmFormFieldVO formField = new BpmFormFieldVO()
|
||||||
|
.setType(JsonUtils.getText(formFieldNode, "type"))
|
||||||
|
.setField(JsonUtils.getText(formFieldNode, "field"))
|
||||||
|
.setTitle(JsonUtils.getText(formFieldNode, "title"));
|
||||||
if (StrUtil.isNotBlank(formField.getField())) {
|
if (StrUtil.isNotBlank(formField.getField())) {
|
||||||
formFieldsMap.put(formField.getField(), formField);
|
formFieldsMap.put(formField.getField(), formField);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import cn.iocoder.yudao.module.bpm.dal.redis.BpmProcessIdRedisDAO;
|
||||||
import cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants;
|
import cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants;
|
||||||
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
|
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
|
||||||
import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeTypeEnum;
|
import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeTypeEnum;
|
||||||
|
import cn.iocoder.yudao.module.bpm.enums.task.BpmAttachmentTypeEnum;
|
||||||
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
|
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
|
||||||
import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
|
import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
|
||||||
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
|
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
|
||||||
|
|
@ -55,6 +56,7 @@ import org.flowable.engine.repository.ProcessDefinition;
|
||||||
import org.flowable.engine.runtime.Execution;
|
import org.flowable.engine.runtime.Execution;
|
||||||
import org.flowable.engine.runtime.ProcessInstance;
|
import org.flowable.engine.runtime.ProcessInstance;
|
||||||
import org.flowable.engine.runtime.ProcessInstanceBuilder;
|
import org.flowable.engine.runtime.ProcessInstanceBuilder;
|
||||||
|
import org.flowable.engine.task.Attachment;
|
||||||
import org.flowable.task.api.Task;
|
import org.flowable.task.api.Task;
|
||||||
import org.flowable.task.api.history.HistoricTaskInstance;
|
import org.flowable.task.api.history.HistoricTaskInstance;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
|
@ -404,8 +406,13 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
|
||||||
// 遍历 tasks 列表,只处理已结束的 UserTask
|
// 遍历 tasks 列表,只处理已结束的 UserTask
|
||||||
// 为什么不通过 activities 呢?因为,加签场景下,它只存在于 tasks,没有 activities,导致如果遍历 activities 的话,它无法成为一个节点
|
// 为什么不通过 activities 呢?因为,加签场景下,它只存在于 tasks,没有 activities,导致如果遍历 activities 的话,它无法成为一个节点
|
||||||
List<HistoricTaskInstance> endTasks = filterList(tasks, task -> task.getEndTime() != null);
|
List<HistoricTaskInstance> endTasks = filterList(tasks, task -> task.getEndTime() != null);
|
||||||
|
// 获取已完成节点的附件
|
||||||
|
Set<String> endTaskIds = convertSet(endTasks, HistoricTaskInstance::getId);
|
||||||
|
List<Attachment> attachments = taskService.getAttachments(historicProcessInstance.getId(), endTaskIds, BpmAttachmentTypeEnum.TASK_ATTACHMENT);
|
||||||
|
Map<String, List<Attachment>> taskAttachmentMap = convertMultiMap(attachments, Attachment::getTaskId);
|
||||||
List<ActivityNode> approvalNodes = convertList(endTasks, task -> {
|
List<ActivityNode> approvalNodes = convertList(endTasks, task -> {
|
||||||
FlowElement flowNode = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
|
FlowElement flowNode = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
|
||||||
|
List<Attachment> taskAttachments = taskAttachmentMap.get(task.getId());
|
||||||
ActivityNode activityNode = new ActivityNode().setId(task.getTaskDefinitionKey()).setName(task.getName())
|
ActivityNode activityNode = new ActivityNode().setId(task.getTaskDefinitionKey()).setName(task.getName())
|
||||||
.setNodeType(START_USER_NODE_ID.equals(task.getTaskDefinitionKey())
|
.setNodeType(START_USER_NODE_ID.equals(task.getTaskDefinitionKey())
|
||||||
? BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType()
|
? BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType()
|
||||||
|
|
@ -414,7 +421,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
|
||||||
.setStatus(getEndActivityNodeStatus(task))
|
.setStatus(getEndActivityNodeStatus(task))
|
||||||
.setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode))
|
.setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode))
|
||||||
.setStartTime(DateUtils.of(task.getCreateTime())).setEndTime(DateUtils.of(task.getEndTime()))
|
.setStartTime(DateUtils.of(task.getCreateTime())).setEndTime(DateUtils.of(task.getEndTime()))
|
||||||
.setTasks(singletonList(BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(task)));
|
.setTasks(singletonList(BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(task, taskAttachments)));
|
||||||
// 如果是取消状态,则跳过
|
// 如果是取消状态,则跳过
|
||||||
if (BpmTaskStatusEnum.isCancelStatus(activityNode.getStatus())) {
|
if (BpmTaskStatusEnum.isCancelStatus(activityNode.getStatus())) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -528,14 +535,14 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
|
||||||
if (task == null) {
|
if (task == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
activityNode.getTasks().add(BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(task));
|
activityNode.getTasks().add(BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(task, null));
|
||||||
// 加签子任务,需要过滤掉已经完成的加签子任务
|
// 加签子任务,需要过滤掉已经完成的加签子任务
|
||||||
List<HistoricTaskInstance> childrenTasks = filterList(
|
List<HistoricTaskInstance> childrenTasks = filterList(
|
||||||
taskService.getAllChildrenTaskListByParentTaskId(activity.getTaskId(), tasks),
|
taskService.getAllChildrenTaskListByParentTaskId(activity.getTaskId(), tasks),
|
||||||
childTask -> childTask.getEndTime() == null);
|
childTask -> childTask.getEndTime() == null);
|
||||||
if (CollUtil.isNotEmpty(childrenTasks)) {
|
if (CollUtil.isNotEmpty(childrenTasks)) {
|
||||||
activityNode.getTasks().addAll(
|
activityNode.getTasks().addAll(
|
||||||
convertList(childrenTasks, BpmProcessInstanceConvert.INSTANCE::buildApprovalTaskInfo));
|
convertList(childrenTasks, item->BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(item, null)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 处理每个任务的 candidateUsers 属性:如果是依次审批,需要预测它的后续审批人。因为 Task 是审批完一个,创建一个新的 Task
|
// 处理每个任务的 candidateUsers 属性:如果是依次审批,需要预测它的后续审批人。因为 Task 是审批完一个,创建一个新的 Task
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||||
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
|
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
|
||||||
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*;
|
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*;
|
||||||
import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskTimeoutHandlerTypeEnum;
|
import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskTimeoutHandlerTypeEnum;
|
||||||
|
import cn.iocoder.yudao.module.bpm.enums.task.BpmAttachmentTypeEnum;
|
||||||
import org.flowable.bpmn.model.UserTask;
|
import org.flowable.bpmn.model.UserTask;
|
||||||
import org.flowable.engine.history.HistoricActivityInstance;
|
import org.flowable.engine.history.HistoricActivityInstance;
|
||||||
|
import org.flowable.engine.task.Attachment;
|
||||||
import org.flowable.task.api.Task;
|
import org.flowable.task.api.Task;
|
||||||
import org.flowable.task.api.TaskInfo;
|
import org.flowable.task.api.TaskInfo;
|
||||||
import org.flowable.task.api.history.HistoricTaskInstance;
|
import org.flowable.task.api.history.HistoricTaskInstance;
|
||||||
|
|
@ -14,6 +16,7 @@ import javax.validation.Valid;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 流程任务实例 Service 接口
|
* 流程任务实例 Service 接口
|
||||||
|
|
@ -184,6 +187,17 @@ public interface BpmTaskService {
|
||||||
*/
|
*/
|
||||||
List<HistoricTaskInstance> getFinishedTaskListByProcessInstanceIdWithoutCancel(String processInstanceId);
|
List<HistoricTaskInstance> getFinishedTaskListByProcessInstanceIdWithoutCancel(String processInstanceId);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据条件获取附件
|
||||||
|
*
|
||||||
|
* @param processInstanceId 流程 id
|
||||||
|
* @param taskIds 任务 id 集合
|
||||||
|
* @param attachmentType 附件类型
|
||||||
|
* @return 附件集合
|
||||||
|
*/
|
||||||
|
List<Attachment> getAttachments(String processInstanceId, Set<String> taskIds, BpmAttachmentTypeEnum attachmentType);
|
||||||
|
|
||||||
// ========== Update 写入相关方法 ==========
|
// ========== Update 写入相关方法 ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.service.task;
|
||||||
|
|
||||||
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.collection.CollUtil;
|
||||||
import cn.hutool.core.convert.Convert;
|
import cn.hutool.core.convert.Convert;
|
||||||
|
import cn.hutool.core.io.FileUtil;
|
||||||
import cn.hutool.core.lang.Assert;
|
import cn.hutool.core.lang.Assert;
|
||||||
import cn.hutool.core.util.*;
|
import cn.hutool.core.util.*;
|
||||||
import cn.hutool.extra.spring.SpringUtil;
|
import cn.hutool.extra.spring.SpringUtil;
|
||||||
|
|
@ -19,10 +20,7 @@ import cn.iocoder.yudao.module.bpm.convert.task.BpmTaskConvert;
|
||||||
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
|
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
|
||||||
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
|
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
|
||||||
import cn.iocoder.yudao.module.bpm.enums.definition.*;
|
import cn.iocoder.yudao.module.bpm.enums.definition.*;
|
||||||
import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum;
|
import cn.iocoder.yudao.module.bpm.enums.task.*;
|
||||||
import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
|
|
||||||
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskSignTypeEnum;
|
|
||||||
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
|
|
||||||
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
|
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
|
||||||
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
|
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
|
||||||
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils;
|
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils;
|
||||||
|
|
@ -47,6 +45,7 @@ import org.flowable.engine.history.HistoricActivityInstance;
|
||||||
import org.flowable.engine.runtime.ActivityInstance;
|
import org.flowable.engine.runtime.ActivityInstance;
|
||||||
import org.flowable.engine.runtime.Execution;
|
import org.flowable.engine.runtime.Execution;
|
||||||
import org.flowable.engine.runtime.ProcessInstance;
|
import org.flowable.engine.runtime.ProcessInstance;
|
||||||
|
import org.flowable.engine.task.Attachment;
|
||||||
import org.flowable.task.api.DelegationState;
|
import org.flowable.task.api.DelegationState;
|
||||||
import org.flowable.task.api.Task;
|
import org.flowable.task.api.Task;
|
||||||
import org.flowable.task.api.TaskInfo;
|
import org.flowable.task.api.TaskInfo;
|
||||||
|
|
@ -510,6 +509,18 @@ public class BpmTaskServiceImpl implements BpmTaskService {
|
||||||
.orderByHistoricTaskInstanceStartTime().asc().list();
|
.orderByHistoricTaskInstanceStartTime().asc().list();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Attachment> getAttachments(String processInstanceId, Set<String> taskIds, BpmAttachmentTypeEnum attachmentType) {
|
||||||
|
List<Attachment> result = taskService.getProcessInstanceAttachments(processInstanceId);
|
||||||
|
if (CollUtil.isNotEmpty(taskIds)) {
|
||||||
|
result = filterList(result, attachment -> taskIds.contains(attachment.getTaskId()));
|
||||||
|
}
|
||||||
|
if (attachmentType != null) {
|
||||||
|
result = filterList(result, attachment -> attachmentType.getType().equals(attachment.getType()));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断指定用户,是否是当前任务的审批人
|
* 判断指定用户,是否是当前任务的审批人
|
||||||
*
|
*
|
||||||
|
|
@ -592,6 +603,11 @@ public class BpmTaskServiceImpl implements BpmTaskService {
|
||||||
// 2.2 添加评论
|
// 2.2 添加评论
|
||||||
taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.APPROVE.getType(),
|
taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.APPROVE.getType(),
|
||||||
BpmCommentTypeEnum.APPROVE.formatComment(reqVO.getReason()));
|
BpmCommentTypeEnum.APPROVE.formatComment(reqVO.getReason()));
|
||||||
|
// 2.3 添加附件
|
||||||
|
if (CollUtil.isNotEmpty(reqVO.getAttachments())) {
|
||||||
|
reqVO.getAttachments().forEach(attachment -> taskService.createAttachment(BpmAttachmentTypeEnum.TASK_ATTACHMENT.getType(),
|
||||||
|
task.getId(), task.getProcessInstanceId(), FileUtil.getName(URLUtil.getPath(attachment)), null, attachment));
|
||||||
|
}
|
||||||
|
|
||||||
// 3. 设置流程变量。如果流程变量前端传空,需要从历史实例中获取,原因:前端表单如果在当前节点无可编辑的字段时 variables 一定会为空
|
// 3. 设置流程变量。如果流程变量前端传空,需要从历史实例中获取,原因:前端表单如果在当前节点无可编辑的字段时 variables 一定会为空
|
||||||
// 场景一:A 节点发起,B 节点表单无可编辑字段,审批通过时,C 节点需要流程变量获取下一个执行节点,但因为 B 节点无可编辑的字段,variables 为空,流程可能出现问题。
|
// 场景一:A 节点发起,B 节点表单无可编辑字段,审批通过时,C 节点需要流程变量获取下一个执行节点,但因为 B 节点无可编辑的字段,variables 为空,流程可能出现问题。
|
||||||
|
|
@ -618,7 +634,14 @@ public class BpmTaskServiceImpl implements BpmTaskService {
|
||||||
runtimeService.setVariable(task.getProcessInstanceId(), BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_TASK_IDS, needSimulateTaskIdsByReturn);
|
runtimeService.setVariable(task.getProcessInstanceId(), BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_TASK_IDS, needSimulateTaskIdsByReturn);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 调用 BPM complete 去完成任务
|
// 6. 清理退回设置的不自动通过的变量。仅在该标记存在时才删除,避免每次完成任务都产生无谓的 DB delete
|
||||||
|
String returnFlagKey = String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey());
|
||||||
|
if (runtimeService.hasVariable(task.getProcessInstanceId(), returnFlagKey)) {
|
||||||
|
log.info("[approveTask][taskId({}) 清理退回标记变量({})]", task.getId(), returnFlagKey);
|
||||||
|
runtimeService.removeVariable(task.getProcessInstanceId(), returnFlagKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 调用 BPM complete 去完成任务
|
||||||
taskService.complete(task.getId(), variables, true);
|
taskService.complete(task.getId(), variables, true);
|
||||||
|
|
||||||
// 【加签专属】处理加签任务
|
// 【加签专属】处理加签任务
|
||||||
|
|
@ -810,7 +833,13 @@ public class BpmTaskServiceImpl implements BpmTaskService {
|
||||||
// 2.2 添加流程评论
|
// 2.2 添加流程评论
|
||||||
taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.REJECT.getType(),
|
taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.REJECT.getType(),
|
||||||
BpmCommentTypeEnum.REJECT.formatComment(reqVO.getReason()));
|
BpmCommentTypeEnum.REJECT.formatComment(reqVO.getReason()));
|
||||||
// 2.3 如果当前任务时被加签的,则加它的根任务也标记成未通过
|
// 2.3 添加附件
|
||||||
|
if (CollUtil.isNotEmpty(reqVO.getAttachments())) {
|
||||||
|
reqVO.getAttachments().forEach(attachment -> taskService.createAttachment(BpmAttachmentTypeEnum.TASK_ATTACHMENT.getType(),
|
||||||
|
task.getId(), task.getProcessInstanceId(), FileUtil.getName(URLUtil.getPath(attachment)), null, attachment));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.4 如果当前任务时被加签的,则加它的根任务也标记成未通过
|
||||||
// 疑问:为什么要标记未通过呢?
|
// 疑问:为什么要标记未通过呢?
|
||||||
// 回答:例如说 A 任务被向前加签除 B 任务时,B 任务被审批不通过,此时 A 会被取消。而 yudao-ui-admin-vue3 不展示“已取消”的任务,导致展示不出审批不通过的细节。
|
// 回答:例如说 A 任务被向前加签除 B 任务时,B 任务被审批不通过,此时 A 会被取消。而 yudao-ui-admin-vue3 不展示“已取消”的任务,导致展示不出审批不通过的细节。
|
||||||
if (task.getParentTaskId() != null) {
|
if (task.getParentTaskId() != null) {
|
||||||
|
|
@ -922,14 +951,12 @@ public class BpmTaskServiceImpl implements BpmTaskService {
|
||||||
// 为什么不直接使用 runTaskKeyList 呢?因为可能存在多个审批分支,例如说:A -> B -> C 和 D -> F,而只要 C 撤回到 A,需要排除掉 F
|
// 为什么不直接使用 runTaskKeyList 呢?因为可能存在多个审批分支,例如说:A -> B -> C 和 D -> F,而只要 C 撤回到 A,需要排除掉 F
|
||||||
List<UserTask> returnUserTaskList = BpmnModelUtils.iteratorFindChildUserTasks(targetElement, runTaskKeyList, null, null);
|
List<UserTask> returnUserTaskList = BpmnModelUtils.iteratorFindChildUserTasks(targetElement, runTaskKeyList, null, null);
|
||||||
List<String> returnTaskKeyList = convertList(returnUserTaskList, UserTask::getId);
|
List<String> returnTaskKeyList = convertList(returnUserTaskList, UserTask::getId);
|
||||||
|
|
||||||
// 2. 给当前要被退回的 task 数组,设置退回意见
|
// 2. 给当前要被退回的 task 数组,设置退回意见
|
||||||
taskList.forEach(task -> {
|
taskList.forEach(task -> {
|
||||||
// 需要排除掉,不需要设置退回意见的任务
|
// 需要排除掉,不需要设置退回意见的任务
|
||||||
if (!returnTaskKeyList.contains(task.getTaskDefinitionKey())) {
|
if (!returnTaskKeyList.contains(task.getTaskDefinitionKey())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否分配给自己任务,因为会签任务,一个节点会有多个任务
|
// 判断是否分配给自己任务,因为会签任务,一个节点会有多个任务
|
||||||
if (isAssignUserTask(userId, task)) { // 情况一:自己的任务,进行 RETURN 标记
|
if (isAssignUserTask(userId, task)) { // 情况一:自己的任务,进行 RETURN 标记
|
||||||
// 2.1.1 添加评论
|
// 2.1.1 添加评论
|
||||||
|
|
@ -950,14 +977,17 @@ public class BpmTaskServiceImpl implements BpmTaskService {
|
||||||
// 相关 issue: https://github.com/flowable/flowable-engine/issues/3944
|
// 相关 issue: https://github.com/flowable/flowable-engine/issues/3944
|
||||||
// ② flowable 7.2.0 版本后,继续使用 moveActivityIdsToSingleActivityId 方法。原因:flowable 7.2.0 版本修复了该问题。
|
// ② flowable 7.2.0 版本后,继续使用 moveActivityIdsToSingleActivityId 方法。原因:flowable 7.2.0 版本修复了该问题。
|
||||||
// 相关 issue:https://github.com/YunaiV/ruoyi-vue-pro/issues/1018
|
// 相关 issue:https://github.com/YunaiV/ruoyi-vue-pro/issues/1018
|
||||||
|
// ③ moveActivityIdsToSingleActivityId 使用遇到问题, 相关 issue https://gitee.com/zhijiantianya/yudao-cloud/issues/IJM8MS
|
||||||
|
// 改成 moveExecutionsToSingleActivityId 好像并没有遇到 ② 提到的超时提醒失效的问题。暂时先改回 moveExecutionsToSingleActivityId
|
||||||
|
// ④ moveExecutionsToSingleActivityId 回退多实例的时候不会去删除多实例根, 应改成 moveActivityIdsToSingleActivityId
|
||||||
|
// flowable 8.0.0 修复上面相关问题, 还修复了并行分支回退的问题 https://t.zsxq.com/z4d9i。
|
||||||
runtimeService.createChangeActivityStateBuilder()
|
runtimeService.createChangeActivityStateBuilder()
|
||||||
.processInstanceId(currentTask.getProcessInstanceId())
|
.processInstanceId(currentTask.getProcessInstanceId())
|
||||||
.moveActivityIdsToSingleActivityId(returnTaskKeyList, reqVO.getTargetTaskDefinitionKey())
|
.moveActivityIdsToSingleActivityId(returnTaskKeyList, reqVO.getTargetTaskDefinitionKey())
|
||||||
// 设置需要预测的任务 ids 的流程变量,用于辅助预测
|
// 设置需要预测的任务 ids 的流程变量,用于辅助预测
|
||||||
.processVariable(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_TASK_IDS, needSimulateTaskDefinitionKeys)
|
.processVariable(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_TASK_IDS, needSimulateTaskDefinitionKeys)
|
||||||
// 设置流程变量(local)节点退回标记, 用于退回到节点,不执行 BpmUserTaskAssignStartUserHandlerTypeEnum 策略,导致自动通过
|
// 设置流程变量节点退回标记, 用于退回到节点,不执行 BpmUserTaskAssignStartUserHandlerTypeEnum 策略,导致自动通过
|
||||||
.localVariable(reqVO.getTargetTaskDefinitionKey(),
|
.processVariable(String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, reqVO.getTargetTaskDefinitionKey()), Boolean.TRUE)
|
||||||
String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, reqVO.getTargetTaskDefinitionKey()), Boolean.TRUE)
|
|
||||||
.changeState();
|
.changeState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1464,101 +1494,105 @@ public class BpmTaskServiceImpl implements BpmTaskService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自动去重,通过自动审批的方式
|
// 需要基于 instance 设置租户编号,避免 Flowable 内部异步执行时【例如:超时自动通过】 丢失租户编号
|
||||||
BpmProcessDefinitionInfoDO processDefinitionInfo = bpmProcessDefinitionService.getProcessDefinitionInfo(task.getProcessDefinitionId());
|
FlowableUtils.execute(processInstance.getTenantId(), () -> {
|
||||||
if (processDefinitionInfo == null) {
|
// 自动去重,通过自动审批的方式
|
||||||
log.error("[processTaskAssigned][taskId({}) 没有找到流程定义({})]", task.getId(), task.getProcessDefinitionId());
|
BpmProcessDefinitionInfoDO processDefinitionInfo = bpmProcessDefinitionService.getProcessDefinitionInfo(task.getProcessDefinitionId());
|
||||||
return;
|
if (processDefinitionInfo == null) {
|
||||||
}
|
log.error("[processTaskAssigned][taskId({}) 没有找到流程定义({})]", task.getId(), task.getProcessDefinitionId());
|
||||||
if (processDefinitionInfo.getAutoApprovalType() != null) {
|
|
||||||
HistoricTaskInstanceQuery sameAssigneeQuery = historyService.createHistoricTaskInstanceQuery()
|
|
||||||
.processInstanceId(task.getProcessInstanceId())
|
|
||||||
.taskAssignee(task.getAssignee()) // 相同审批人
|
|
||||||
.taskVariableValueEquals(BpmnVariableConstants.TASK_VARIABLE_STATUS, BpmTaskStatusEnum.APPROVE.getStatus())
|
|
||||||
.finished();
|
|
||||||
if (BpmAutoApproveTypeEnum.APPROVE_ALL.getType().equals(processDefinitionInfo.getAutoApprovalType())
|
|
||||||
&& sameAssigneeQuery.count() > 0) {
|
|
||||||
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
|
|
||||||
.setReason(BpmAutoApproveTypeEnum.APPROVE_ALL.getName()));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (BpmAutoApproveTypeEnum.APPROVE_SEQUENT.getType().equals(processDefinitionInfo.getAutoApprovalType())) {
|
if (processDefinitionInfo.getAutoApprovalType() != null) {
|
||||||
BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId());
|
HistoricTaskInstanceQuery approvedTaskQuery = historyService.createHistoricTaskInstanceQuery()
|
||||||
if (bpmnModel == null) {
|
.processInstanceId(task.getProcessInstanceId())
|
||||||
log.error("[processTaskAssigned][taskId({}) 没有找到流程模型({})]", task.getId(), task.getProcessDefinitionId());
|
.taskVariableValueEquals(BpmnVariableConstants.TASK_VARIABLE_STATUS, BpmTaskStatusEnum.APPROVE.getStatus())
|
||||||
return;
|
.finished();
|
||||||
}
|
if (BpmAutoApproveTypeEnum.APPROVE_ALL.getType().equals(processDefinitionInfo.getAutoApprovalType())
|
||||||
List<String> sourceTaskIds = convertList(BpmnModelUtils.getElementIncomingFlows( // 获取所有上一个节点
|
&& approvedTaskQuery.taskAssignee(task.getAssignee()).count() > 0) {
|
||||||
BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey())),
|
|
||||||
SequenceFlow::getSourceRef);
|
|
||||||
if (sameAssigneeQuery.taskDefinitionKeys(sourceTaskIds).count() > 0) {
|
|
||||||
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
|
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
|
||||||
.setReason(BpmAutoApproveTypeEnum.APPROVE_SEQUENT.getName()));
|
.setReason(BpmAutoApproveTypeEnum.APPROVE_ALL.getName()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
// 连续审批的节点自动通过
|
||||||
}
|
if (BpmAutoApproveTypeEnum.APPROVE_SEQUENT.getType().equals(processDefinitionInfo.getAutoApprovalType())) {
|
||||||
|
BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId());
|
||||||
// 获取发起人节点
|
if (bpmnModel == null) {
|
||||||
BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId());
|
log.error("[processTaskAssigned][taskId({}) 没有找到流程模型({})]", task.getId(), task.getProcessDefinitionId());
|
||||||
if (bpmnModel == null) {
|
return;
|
||||||
log.error("[processTaskAssigned][taskId({}) 没有找到流程模型]", task.getId());
|
}
|
||||||
return;
|
List<String> sourceTaskIds = convertList(BpmnModelUtils.getElementIncomingUserTaskFlows( // 获取所有的上一个 UserTask 节点连线
|
||||||
}
|
BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey())),
|
||||||
FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
|
SequenceFlow::getSourceRef);
|
||||||
// 判断是否为退回或者驳回:如果是退回或者驳回不走这个策略(使用 local variable)
|
approvedTaskQuery.taskDefinitionKeys(sourceTaskIds).orderByTaskCreateTime().desc(); // 设置 taskIds, 并按创建时间倒序排序
|
||||||
Boolean returnTaskFlag = runtimeService.getVariableLocal(task.getExecutionId(),
|
HistoricTaskInstance firstHisTask = CollUtil.getFirst(approvedTaskQuery.list());
|
||||||
String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey()), Boolean.class);
|
if (firstHisTask != null && StrUtil.equals(firstHisTask.getAssignee(), task.getAssignee())) {
|
||||||
Boolean skipStartUserNodeFlag = Convert.toBool(runtimeService.getVariable(processInstance.getProcessInstanceId(),
|
|
||||||
BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class));
|
|
||||||
if (userTaskElement.getId().equals(START_USER_NODE_ID)
|
|
||||||
&& (skipStartUserNodeFlag == null // 目的:一般是“主流程”,发起人节点,自动通过审核
|
|
||||||
|| BooleanUtil.isTrue(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核
|
|
||||||
&& ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) {
|
|
||||||
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
|
|
||||||
.setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE.getReason()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 当不为发起人节点时,审批人与提交人为同一人时,根据 BpmUserTaskAssignStartUserHandlerTypeEnum 策略进行处理
|
|
||||||
if (ObjectUtil.notEqual(userTaskElement.getId(), START_USER_NODE_ID)
|
|
||||||
&& StrUtil.equals(task.getAssignee(), processInstance.getStartUserId())) {
|
|
||||||
if (ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) {
|
|
||||||
Integer assignStartUserHandlerType = BpmnModelUtils.parseAssignStartUserHandlerType(userTaskElement);
|
|
||||||
|
|
||||||
// 情况一:自动跳过
|
|
||||||
if (ObjectUtils.equalsAny(assignStartUserHandlerType,
|
|
||||||
BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType())) {
|
|
||||||
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
|
|
||||||
.setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP.getReason()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 情况二:转交给部门负责人审批
|
|
||||||
if (ObjectUtils.equalsAny(assignStartUserHandlerType,
|
|
||||||
BpmUserTaskAssignStartUserHandlerTypeEnum.TRANSFER_DEPT_LEADER.getType())) {
|
|
||||||
AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId())).getCheckedData();
|
|
||||||
Assert.notNull(startUser, "提交人({})信息为空", processInstance.getStartUserId());
|
|
||||||
DeptRespDTO dept = startUser.getDeptId() != null ? deptApi.getDept(startUser.getDeptId()).getCheckedData() : null;
|
|
||||||
Assert.notNull(dept, "提交人({})部门({})信息为空", processInstance.getStartUserId(), startUser.getDeptId());
|
|
||||||
// 找不到部门负责人的情况下,自动审批通过
|
|
||||||
// noinspection DataFlowIssue
|
|
||||||
if (dept.getLeaderUserId() == null) {
|
|
||||||
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
|
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
|
||||||
.setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_DEPT_LEADER_NOT_FOUND.getReason()));
|
.setReason(BpmAutoApproveTypeEnum.APPROVE_SEQUENT.getName()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 找得到部门负责人的情况下,修改负责人
|
|
||||||
if (ObjectUtil.notEqual(dept.getLeaderUserId(), startUser.getId())) {
|
|
||||||
getSelf().transferTask(Long.valueOf(task.getAssignee()), new BpmTaskTransferReqVO()
|
|
||||||
.setId(task.getId()).setAssigneeUserId(dept.getLeaderUserId())
|
|
||||||
.setReason(BpmReasonEnum.ASSIGN_START_USER_TRANSFER_DEPT_LEADER.getReason()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 如果部门负责人是自己,还是自己审批吧~
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// 注意:需要基于 instance 设置租户编号,避免 Flowable 内部异步时,丢失租户编号
|
// 获取发起人节点
|
||||||
FlowableUtils.execute(processInstance.getTenantId(), () -> {
|
BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId());
|
||||||
|
if (bpmnModel == null) {
|
||||||
|
log.error("[processTaskAssigned][taskId({}) 没有找到流程模型]", task.getId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
|
||||||
|
// 判断是否为退回或者驳回:如果是退回或者驳回不走这个策略
|
||||||
|
Boolean returnTaskFlag = runtimeService.getVariable(processInstance.getProcessInstanceId(),
|
||||||
|
String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey()), Boolean.class);
|
||||||
|
Boolean skipStartUserNodeFlag = Convert.toBool(runtimeService.getVariable(processInstance.getProcessInstanceId(),
|
||||||
|
BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class));
|
||||||
|
if (userTaskElement.getId().equals(START_USER_NODE_ID)
|
||||||
|
&& (skipStartUserNodeFlag == null // 目的:一般是“主流程”,发起人节点,自动通过审核
|
||||||
|
|| BooleanUtil.isTrue(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核
|
||||||
|
&& ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) {
|
||||||
|
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
|
||||||
|
.setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE.getReason()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 当不为发起人节点时,审批人与提交人为同一人时,根据 BpmUserTaskAssignStartUserHandlerTypeEnum 策略进行处理
|
||||||
|
if (ObjectUtil.notEqual(userTaskElement.getId(), START_USER_NODE_ID)
|
||||||
|
&& StrUtil.equals(task.getAssignee(), processInstance.getStartUserId())) {
|
||||||
|
if (ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) {
|
||||||
|
Integer assignStartUserHandlerType = BpmnModelUtils.parseAssignStartUserHandlerType(userTaskElement);
|
||||||
|
|
||||||
|
// 情况一:自动跳过
|
||||||
|
if (ObjectUtils.equalsAny(assignStartUserHandlerType,
|
||||||
|
BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType())) {
|
||||||
|
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
|
||||||
|
.setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP.getReason()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 情况二:转交给部门负责人审批
|
||||||
|
if (ObjectUtils.equalsAny(assignStartUserHandlerType,
|
||||||
|
BpmUserTaskAssignStartUserHandlerTypeEnum.TRANSFER_DEPT_LEADER.getType())) {
|
||||||
|
AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId())).getCheckedData();
|
||||||
|
Assert.notNull(startUser, "提交人({})信息为空", processInstance.getStartUserId());
|
||||||
|
DeptRespDTO dept = startUser.getDeptId() != null ? deptApi.getDept(startUser.getDeptId()).getCheckedData() : null;
|
||||||
|
Assert.notNull(dept, "提交人({})部门({})信息为空", processInstance.getStartUserId(), startUser.getDeptId());
|
||||||
|
// 找不到部门负责人的情况下,自动审批通过
|
||||||
|
// noinspection DataFlowIssue
|
||||||
|
if (dept.getLeaderUserId() == null) {
|
||||||
|
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
|
||||||
|
.setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_DEPT_LEADER_NOT_FOUND.getReason()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 找得到部门负责人的情况下,修改负责人
|
||||||
|
if (ObjectUtil.notEqual(dept.getLeaderUserId(), startUser.getId())) {
|
||||||
|
getSelf().transferTask(Long.valueOf(task.getAssignee()), new BpmTaskTransferReqVO()
|
||||||
|
.setId(task.getId()).setAssigneeUserId(dept.getLeaderUserId())
|
||||||
|
.setReason(BpmReasonEnum.ASSIGN_START_USER_TRANSFER_DEPT_LEADER.getReason()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 如果部门负责人是自己,还是自己审批吧~
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId())).getCheckedData();
|
AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId())).getCheckedData();
|
||||||
messageService.sendMessageWhenTaskAssigned(BpmTaskConvert.INSTANCE.convert(processInstance, startUser, task));
|
messageService.sendMessageWhenTaskAssigned(BpmTaskConvert.INSTANCE.convert(processInstance, startUser, task));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ spring:
|
||||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro-jdk8-new?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
|
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro-jdk8-new?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
|
||||||
# url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # MySQL Connector/J 5.X 连接的示例
|
# url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # MySQL Connector/J 5.X 连接的示例
|
||||||
# url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例
|
# url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例
|
||||||
|
# url: jdbc:postgresql://127.0.0.1:5866/ruoyi-vue-pro # 瀚高 HighGo 连接的示例
|
||||||
# url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
|
# url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
|
||||||
# url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro # SQLServer 连接的示例
|
# url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro # SQLServer 连接的示例
|
||||||
# url: jdbc:dm://10.211.55.4:5236?schema=RUOYI_VUE_PRO # DM 连接的示例
|
# url: jdbc:dm://10.211.55.4:5236?schema=RUOYI_VUE_PRO # DM 连接的示例
|
||||||
|
|
@ -70,6 +71,8 @@ spring:
|
||||||
# password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W # SQL Server 连接的示例
|
# password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W # SQL Server 连接的示例
|
||||||
# username: SYSDBA # DM 连接的示例
|
# username: SYSDBA # DM 连接的示例
|
||||||
# password: SYSDBA # DM 连接的示例
|
# password: SYSDBA # DM 连接的示例
|
||||||
|
# username: root # 瀚高 HighGo 连接的示例
|
||||||
|
# password: 123456 # 瀚高 HighGo 连接的示例
|
||||||
slave: # 模拟从库,可根据自己需要修改
|
slave: # 模拟从库,可根据自己需要修改
|
||||||
lazy: true # 开启懒加载,保证启动速度
|
lazy: true # 开启懒加载,保证启动速度
|
||||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro-jdk8-new?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro-jdk8-new?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,7 @@ public class BpmTaskCandidateInvokerTest extends BaseMockitoUnitTest {
|
||||||
when(adminUserApi.getUserMap(eq(asSet(1L, 2L)))).thenReturn(userMap);
|
when(adminUserApi.getUserMap(eq(asSet(1L, 2L)))).thenReturn(userMap);
|
||||||
// mock 方法(empty)
|
// mock 方法(empty)
|
||||||
when(emptyStrategy.calculateUsersByActivity(same(bpmnModel), eq(activityId),
|
when(emptyStrategy.calculateUsersByActivity(same(bpmnModel), eq(activityId),
|
||||||
eq(param), same(startUserId), same(processDefinitionId), same(processVariables)))
|
eq(param), same(startUserId), same(processDefinitionId), same(processVariables)))
|
||||||
.thenReturn(Sets.newSet(2L));
|
.thenReturn(Sets.newSet(2L));
|
||||||
|
|
||||||
// 调用
|
// 调用
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
|
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
|
||||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||||
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
|
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
|
||||||
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
|
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
|
||||||
|
|
@ -8,7 +7,6 @@ import org.assertj.core.util.Sets;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.stubbing.Answer;
|
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
|
@ -31,9 +29,10 @@ public class BpmTaskCandidateDeptLeaderMultiStrategyTest extends BaseMockitoUnit
|
||||||
// 准备参数
|
// 准备参数
|
||||||
String param = "10,20|2";
|
String param = "10,20|2";
|
||||||
// mock 方法
|
// mock 方法
|
||||||
when(deptApi.getDept(any())).thenAnswer((Answer< CommonResult<DeptRespDTO>>) invocationOnMock -> {
|
when(deptApi.getDept(any())).thenAnswer(invocationOnMock -> {
|
||||||
Long deptId = invocationOnMock.getArgument(0);
|
Long deptId = invocationOnMock.getArgument(0);
|
||||||
return success(randomPojo(DeptRespDTO.class, o -> o.setId(deptId).setParentId(deptId * 100).setLeaderUserId(deptId + 1)));
|
return success(randomPojo(DeptRespDTO.class,
|
||||||
|
o -> o.setId(deptId).setParentId(deptId * 100).setLeaderUserId(deptId + 1)));
|
||||||
});
|
});
|
||||||
|
|
||||||
// 调用
|
// 调用
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
|
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
|
||||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||||
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
|
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
|
||||||
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
|
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
|
||||||
|
|
@ -13,7 +12,6 @@ import org.flowable.engine.runtime.ProcessInstance;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.stubbing.Answer;
|
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
|
@ -75,9 +73,10 @@ public class BpmTaskCandidateStartUserDeptLeaderMultiStrategyTest extends BaseMo
|
||||||
private void mockGetStartUserDept(Long startUserId) {
|
private void mockGetStartUserDept(Long startUserId) {
|
||||||
when(adminUserApi.getUser(eq(startUserId))).thenReturn(
|
when(adminUserApi.getUser(eq(startUserId))).thenReturn(
|
||||||
success(randomPojo(AdminUserRespDTO.class, o -> o.setId(startUserId).setDeptId(10L))));
|
success(randomPojo(AdminUserRespDTO.class, o -> o.setId(startUserId).setDeptId(10L))));
|
||||||
when(deptApi.getDept(any())).thenAnswer((Answer< CommonResult<DeptRespDTO>>) invocationOnMock -> {
|
when(deptApi.getDept(any())).thenAnswer(invocationOnMock -> {
|
||||||
Long deptId = invocationOnMock.getArgument(0);
|
Long deptId = invocationOnMock.getArgument(0);
|
||||||
return success(randomPojo(DeptRespDTO.class, o -> o.setId(deptId).setParentId(deptId * 100).setLeaderUserId(deptId + 1)));
|
return success(randomPojo(DeptRespDTO.class,
|
||||||
|
o -> o.setId(deptId).setParentId(deptId * 100).setLeaderUserId(deptId + 1)));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
|
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
|
||||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||||
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
|
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
|
||||||
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
|
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
|
||||||
|
|
@ -13,7 +12,6 @@ import org.flowable.engine.runtime.ProcessInstance;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.stubbing.Answer;
|
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
|
@ -75,9 +73,10 @@ public class BpmTaskCandidateStartUserDeptLeaderStrategyTest extends BaseMockito
|
||||||
private void mockGetStartUserDeptLeader(Long startUserId) {
|
private void mockGetStartUserDeptLeader(Long startUserId) {
|
||||||
when(adminUserApi.getUser(eq(startUserId))).thenReturn(
|
when(adminUserApi.getUser(eq(startUserId))).thenReturn(
|
||||||
success(randomPojo(AdminUserRespDTO.class, o -> o.setId(startUserId).setDeptId(10L))));
|
success(randomPojo(AdminUserRespDTO.class, o -> o.setId(startUserId).setDeptId(10L))));
|
||||||
when(deptApi.getDept(any())).thenAnswer((Answer< CommonResult<DeptRespDTO>>) invocationOnMock -> {
|
when(deptApi.getDept(any())).thenAnswer(invocationOnMock -> {
|
||||||
Long deptId = invocationOnMock.getArgument(0);
|
Long deptId = invocationOnMock.getArgument(0);
|
||||||
return success(randomPojo(DeptRespDTO.class, o -> o.setId(deptId).setParentId(deptId * 100).setLeaderUserId(deptId + 1)));
|
return success(randomPojo(DeptRespDTO.class,
|
||||||
|
o -> o.setId(deptId).setParentId(deptId * 100).setLeaderUserId(deptId + 1)));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.common.core.KeyValue;
|
||||||
|
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
|
||||||
|
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
|
||||||
|
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link FlowableUtils} 的单元测试。
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
class FlowableUtilsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetSummary_customSummary_parseDbFormFields() {
|
||||||
|
// 准备参数:模拟 DB 中 form_fields 字段,列表里每个元素都是一个 form-create 字段 JSON。
|
||||||
|
BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionInfo(dbFormFields(),
|
||||||
|
summarySetting(true, "reason", "days", "notExists", "startTime"));
|
||||||
|
Map<String, Object> processVariables = processVariables();
|
||||||
|
|
||||||
|
// 调用
|
||||||
|
List<KeyValue<String, String>> summary = FlowableUtils.getSummary(processDefinitionInfo, processVariables);
|
||||||
|
|
||||||
|
// 断言
|
||||||
|
assertEquals(Arrays.asList(
|
||||||
|
new KeyValue<>("请假原因", "事假"),
|
||||||
|
new KeyValue<>("请假天数", "3"),
|
||||||
|
new KeyValue<>("开始时间", "2026-05-31 09:00:00")),
|
||||||
|
summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetSummary_defaultSummary_parseFirstThreeFieldsByFormOrder() {
|
||||||
|
// 准备参数:未开启自定义摘要时,默认取表单配置顺序里的前三个真实字段。
|
||||||
|
BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionInfo(dbFormFields(), null);
|
||||||
|
Map<String, Object> processVariables = processVariables();
|
||||||
|
|
||||||
|
// 调用
|
||||||
|
List<KeyValue<String, String>> summary = FlowableUtils.getSummary(processDefinitionInfo, processVariables);
|
||||||
|
|
||||||
|
// 断言
|
||||||
|
assertEquals(Arrays.asList(
|
||||||
|
new KeyValue<>("请假原因", "事假"),
|
||||||
|
new KeyValue<>("开始时间", "2026-05-31 09:00:00"),
|
||||||
|
new KeyValue<>("请假天数", "3")),
|
||||||
|
summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetSummary_summaryDisabled_useDefaultSummary() {
|
||||||
|
// 准备参数:摘要设置存在但未启用时,仍走默认摘要逻辑。
|
||||||
|
BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionInfo(dbFormFields(),
|
||||||
|
summarySetting(false, "remark"));
|
||||||
|
Map<String, Object> processVariables = processVariables();
|
||||||
|
|
||||||
|
// 调用
|
||||||
|
List<KeyValue<String, String>> summary = FlowableUtils.getSummary(processDefinitionInfo, processVariables);
|
||||||
|
|
||||||
|
// 断言
|
||||||
|
assertEquals(Arrays.asList(
|
||||||
|
new KeyValue<>("请假原因", "事假"),
|
||||||
|
new KeyValue<>("开始时间", "2026-05-31 09:00:00"),
|
||||||
|
new KeyValue<>("请假天数", "3")),
|
||||||
|
summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetSummary_displayComponentsOnly_returnEmpty() {
|
||||||
|
// 准备参数:分割线、标签、文字等展示组件的 children 是字符串数组,不是表单字段对象。
|
||||||
|
BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionInfo(Arrays.asList(
|
||||||
|
DIVIDER_FIELD,
|
||||||
|
TEXT_FIELD,
|
||||||
|
TAG_FIELD), null);
|
||||||
|
|
||||||
|
// 调用
|
||||||
|
List<KeyValue<String, String>> summary = FlowableUtils.getSummary(processDefinitionInfo,
|
||||||
|
Collections.emptyMap());
|
||||||
|
|
||||||
|
// 断言
|
||||||
|
assertEquals(Collections.emptyList(), summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetSummary_notNormalForm_returnNull() {
|
||||||
|
// 准备参数
|
||||||
|
BpmProcessDefinitionInfoDO processDefinitionInfo = BpmProcessDefinitionInfoDO.builder()
|
||||||
|
.formType(BpmModelFormTypeEnum.CUSTOM.getType())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 调用 & 断言
|
||||||
|
assertNull(FlowableUtils.getSummary(null, Collections.emptyMap()));
|
||||||
|
assertNull(FlowableUtils.getSummary(processDefinitionInfo, Collections.emptyMap()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BpmProcessDefinitionInfoDO processDefinitionInfo(List<String> formFields,
|
||||||
|
BpmModelMetaInfoVO.SummarySetting summarySetting) {
|
||||||
|
return BpmProcessDefinitionInfoDO.builder()
|
||||||
|
.formType(BpmModelFormTypeEnum.NORMAL.getType())
|
||||||
|
.formFields(formFields)
|
||||||
|
.summarySetting(summarySetting)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BpmModelMetaInfoVO.SummarySetting summarySetting(Boolean enable, String... fields) {
|
||||||
|
BpmModelMetaInfoVO.SummarySetting summarySetting = new BpmModelMetaInfoVO.SummarySetting();
|
||||||
|
summarySetting.setEnable(enable);
|
||||||
|
summarySetting.setSummary(Arrays.asList(fields));
|
||||||
|
return summarySetting;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> dbFormFields() {
|
||||||
|
return Arrays.asList(
|
||||||
|
DIVIDER_FIELD,
|
||||||
|
"{\"type\":\"input\",\"field\":\"reason\",\"title\":\"请假原因\",\"value\":\"\","
|
||||||
|
+ "\"props\":{\"type\":\"textarea\",\"placeholder\":\"请输入请假原因\"},"
|
||||||
|
+ "\"$required\":\"请输入请假原因\",\"_fc_id\":\"id_F1\",\"_fc_drag_tag\":\"input\","
|
||||||
|
+ "\"hidden\":false,\"display\":true}",
|
||||||
|
TEXT_FIELD,
|
||||||
|
"{\"type\":\"elRow\",\"title\":\"栅格布局\",\"children\":["
|
||||||
|
+ "{\"type\":\"elCol\",\"props\":{\"span\":12},\"children\":["
|
||||||
|
+ "{\"type\":\"DatePicker\",\"field\":\"startTime\",\"title\":\"开始时间\","
|
||||||
|
+ "\"props\":{\"type\":\"datetime\",\"placeholder\":\"请选择开始时间\"},"
|
||||||
|
+ "\"_fc_id\":\"id_F2\",\"_fc_drag_tag\":\"datePicker\"}]},"
|
||||||
|
+ "\"字段说明\","
|
||||||
|
+ "{\"type\":\"elCol\",\"props\":{\"span\":12},\"children\":["
|
||||||
|
+ "{\"type\":\"inputNumber\",\"field\":\"days\",\"title\":\"请假天数\","
|
||||||
|
+ "\"props\":{\"min\":0,\"precision\":1},\"_fc_id\":\"id_F3\","
|
||||||
|
+ "\"_fc_drag_tag\":\"inputNumber\"}]}],\"_fc_id\":\"id_LAYOUT\","
|
||||||
|
+ "\"_fc_drag_tag\":\"row\"}",
|
||||||
|
TAG_FIELD,
|
||||||
|
"{\"type\":\"input\",\"field\":\"remark\",\"title\":\"备注\",\"value\":\"\","
|
||||||
|
+ "\"props\":{\"placeholder\":\"请输入备注\"},\"_fc_id\":\"id_F4\","
|
||||||
|
+ "\"_fc_drag_tag\":\"input\",\"hidden\":false,\"display\":true}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> processVariables() {
|
||||||
|
Map<String, Object> processVariables = new HashMap<>();
|
||||||
|
processVariables.put("reason", "事假");
|
||||||
|
processVariables.put("startTime", "2026-05-31 09:00:00");
|
||||||
|
processVariables.put("days", 3);
|
||||||
|
processVariables.put("remark", "下午到家");
|
||||||
|
return processVariables;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String DIVIDER_FIELD = "{\"type\":\"elDivider\",\"children\":[\"基础信息\"],"
|
||||||
|
+ "\"props\":{\"contentPosition\":\"left\"},\"_fc_id\":\"id_DIVIDER\","
|
||||||
|
+ "\"_fc_drag_tag\":\"elDivider\"}";
|
||||||
|
|
||||||
|
private static final String TEXT_FIELD = "{\"type\":\"div\",\"children\":[\"请按实际情况填写\"],"
|
||||||
|
+ "\"props\":{\"style\":{\"color\":\"#909399\"}},\"_fc_id\":\"id_TEXT\","
|
||||||
|
+ "\"_fc_drag_tag\":\"text\"}";
|
||||||
|
|
||||||
|
private static final String TAG_FIELD = "{\"type\":\"elTag\",\"children\":[\"重要\"],"
|
||||||
|
+ "\"props\":{\"type\":\"warning\"},\"_fc_id\":\"id_TAG\",\"_fc_drag_tag\":\"elTag\"}";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.crm.dal.mysql.customer;
|
||||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||||
|
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
|
||||||
import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.limitconfig.CrmCustomerLimitConfigPageReqVO;
|
import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.limitconfig.CrmCustomerLimitConfigPageReqVO;
|
||||||
import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerLimitConfigDO;
|
import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerLimitConfigDO;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
@ -28,9 +29,9 @@ public interface CrmCustomerLimitConfigMapper extends BaseMapperX<CrmCustomerLim
|
||||||
LambdaQueryWrapperX<CrmCustomerLimitConfigDO> query = new LambdaQueryWrapperX<CrmCustomerLimitConfigDO>()
|
LambdaQueryWrapperX<CrmCustomerLimitConfigDO> query = new LambdaQueryWrapperX<CrmCustomerLimitConfigDO>()
|
||||||
.eq(CrmCustomerLimitConfigDO::getType, type);
|
.eq(CrmCustomerLimitConfigDO::getType, type);
|
||||||
query.and(w -> {
|
query.and(w -> {
|
||||||
w.apply("FIND_IN_SET({0}, user_ids) > 0", userId);
|
w.apply(MyBatisUtils.findInSet("user_ids"), userId);
|
||||||
if (deptId != null) {
|
if (deptId != null) {
|
||||||
w.or().apply("FIND_IN_SET({0}, dept_ids) > 0", deptId);
|
w.or().apply(MyBatisUtils.findInSet("dept_ids"), deptId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return selectList(query);
|
return selectList(query);
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ spring:
|
||||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
|
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
|
||||||
# url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # MySQL Connector/J 5.X 连接的示例
|
# url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # MySQL Connector/J 5.X 连接的示例
|
||||||
# url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例
|
# url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例
|
||||||
|
# url: jdbc:postgresql://127.0.0.1:5866/ruoyi-vue-pro # 瀚高 HighGo 连接的示例
|
||||||
# url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
|
# url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
|
||||||
# url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro # SQLServer 连接的示例
|
# url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro # SQLServer 连接的示例
|
||||||
# url: jdbc:dm://10.211.55.4:5236?schema=RUOYI_VUE_PRO # DM 连接的示例
|
# url: jdbc:dm://10.211.55.4:5236?schema=RUOYI_VUE_PRO # DM 连接的示例
|
||||||
|
|
@ -69,6 +70,8 @@ spring:
|
||||||
# password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W # SQL Server 连接的示例
|
# password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W # SQL Server 连接的示例
|
||||||
# username: SYSDBA # DM 连接的示例
|
# username: SYSDBA # DM 连接的示例
|
||||||
# password: SYSDBA # DM 连接的示例
|
# password: SYSDBA # DM 连接的示例
|
||||||
|
# username: root # 瀚高 HighGo 连接的示例
|
||||||
|
# password: 123456 # 瀚高 HighGo 连接的示例
|
||||||
slave: # 模拟从库,可根据自己需要修改
|
slave: # 模拟从库,可根据自己需要修改
|
||||||
lazy: true # 开启懒加载,保证启动速度
|
lazy: true # 开启懒加载,保证启动速度
|
||||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ spring:
|
||||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
|
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
|
||||||
# url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # MySQL Connector/J 5.X 连接的示例
|
# url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # MySQL Connector/J 5.X 连接的示例
|
||||||
# url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例
|
# url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例
|
||||||
|
# url: jdbc:postgresql://127.0.0.1:5866/ruoyi-vue-pro # 瀚高 HighGo 连接的示例
|
||||||
# url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
|
# url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
|
||||||
# url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro # SQLServer 连接的示例
|
# url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro # SQLServer 连接的示例
|
||||||
# url: jdbc:dm://10.211.55.4:5236?schema=RUOYI_VUE_PRO # DM 连接的示例
|
# url: jdbc:dm://10.211.55.4:5236?schema=RUOYI_VUE_PRO # DM 连接的示例
|
||||||
|
|
@ -69,6 +70,8 @@ spring:
|
||||||
# password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W # SQL Server 连接的示例
|
# password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W # SQL Server 连接的示例
|
||||||
# username: SYSDBA # DM 连接的示例
|
# username: SYSDBA # DM 连接的示例
|
||||||
# password: SYSDBA # DM 连接的示例
|
# password: SYSDBA # DM 连接的示例
|
||||||
|
# username: root # 瀚高 HighGo 连接的示例
|
||||||
|
# password: 123456 # 瀚高 HighGo 连接的示例
|
||||||
slave: # 模拟从库,可根据自己需要修改
|
slave: # 模拟从库,可根据自己需要修改
|
||||||
lazy: true # 开启懒加载,保证启动速度
|
lazy: true # 开启懒加载,保证启动速度
|
||||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?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>cn.iocoder.cloud</groupId>
|
||||||
|
<artifactId>yudao</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<modules>
|
||||||
|
<module>yudao-module-im-api</module>
|
||||||
|
<module>yudao-module-im-server</module>
|
||||||
|
</modules>
|
||||||
|
<artifactId>yudao-module-im</artifactId>
|
||||||
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
|
<name>${project.artifactId}</name>
|
||||||
|
<description>
|
||||||
|
im 模块,我们放即时通讯业务。
|
||||||
|
例如说:单聊、群聊、消息收发、消息撤回、消息已读等等
|
||||||
|
</description>
|
||||||
|
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?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>cn.iocoder.cloud</groupId>
|
||||||
|
<artifactId>yudao-module-im</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<artifactId>yudao-module-im-api</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<name>${project.artifactId}</name>
|
||||||
|
<description>
|
||||||
|
im 模块 API,暴露给其它模块调用
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.cloud</groupId>
|
||||||
|
<artifactId>yudao-common</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 参数校验 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
/**
|
||||||
|
* @author anhaohao
|
||||||
|
* @since 2024/3/9 下午8:59
|
||||||
|
*/
|
||||||
|
package cn.iocoder.yudao.module.im.api;
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
## AdoptOpenJDK 停止发布 OpenJDK 二进制,而 Eclipse Temurin 是它的延伸,提供更好的稳定性
|
||||||
|
## 感谢复旦核博士的建议!灰子哥,牛皮!
|
||||||
|
FROM eclipse-temurin:21-jre
|
||||||
|
|
||||||
|
## 创建目录,并使用它作为工作目录
|
||||||
|
RUN mkdir -p /yudao-module-im-server
|
||||||
|
WORKDIR /yudao-module-im-server
|
||||||
|
## 将后端项目的 Jar 文件,复制到镜像中
|
||||||
|
COPY ./target/yudao-module-im-server.jar app.jar
|
||||||
|
|
||||||
|
## 设置 TZ 时区
|
||||||
|
## 设置 JAVA_OPTS 环境变量,可通过 docker run -e "JAVA_OPTS=" 进行覆盖
|
||||||
|
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms512m -Xmx512m"
|
||||||
|
|
||||||
|
## 暴露后端项目的 48093 端口
|
||||||
|
EXPOSE 48093
|
||||||
|
|
||||||
|
## 启动后端项目
|
||||||
|
CMD java ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar app.jar
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
<?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>cn.iocoder.cloud</groupId>
|
||||||
|
<artifactId>yudao-module-im</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<artifactId>yudao-module-im-server</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<name>${project.artifactId}</name>
|
||||||
|
<description>
|
||||||
|
im 模块,我们放即时通讯业务。
|
||||||
|
例如说:单聊、群聊、消息收发、消息撤回、消息已读等等
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Spring Cloud 基础 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.cloud</groupId>
|
||||||
|
<artifactId>yudao-spring-boot-starter-env</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 依赖服务 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.cloud</groupId>
|
||||||
|
<artifactId>yudao-module-im-api</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.cloud</groupId>
|
||||||
|
<artifactId>yudao-module-system-api</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.cloud</groupId>
|
||||||
|
<artifactId>yudao-module-infra-api</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 业务组件 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.cloud</groupId>
|
||||||
|
<artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Web 相关 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.cloud</groupId>
|
||||||
|
<artifactId>yudao-spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- DB 相关 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.cloud</groupId>
|
||||||
|
<artifactId>yudao-spring-boot-starter-mybatis</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.cloud</groupId>
|
||||||
|
<artifactId>yudao-spring-boot-starter-redis</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- RPC 远程调用相关 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.cloud</groupId>
|
||||||
|
<artifactId>yudao-spring-boot-starter-rpc</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Registry 注册中心相关 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Config 配置中心相关 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Job 相关 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.cloud</groupId>
|
||||||
|
<artifactId>yudao-spring-boot-starter-job</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 消息队列相关 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.cloud</groupId>
|
||||||
|
<artifactId>yudao-spring-boot-starter-mq</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Test 测试相关 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.cloud</groupId>
|
||||||
|
<artifactId>yudao-spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 工具类相关 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.cloud</groupId>
|
||||||
|
<artifactId>yudao-spring-boot-starter-excel</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 敏感词检测:sensitive-word(trie 树高效匹配,支持忽略大小写 / 全半角 / 数字风格) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.houbb</groupId>
|
||||||
|
<artifactId>sensitive-word</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 汉字转拼音:作为 hutool PinyinUtil 的底层引擎,业务侧调用 StrUtils.toPinyin 时按需引入 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.belerweb</groupId>
|
||||||
|
<artifactId>pinyin4j</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 监控相关 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.cloud</groupId>
|
||||||
|
<artifactId>yudao-spring-boot-starter-monitor</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<!-- 设置构建的 jar 包名 -->
|
||||||
|
<finalName>${project.artifactId}</finalName>
|
||||||
|
<plugins>
|
||||||
|
<!-- 打包 -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<version>${spring.boot.version}</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>repackage</goal> <!-- 将引入的 jar 打入其中 -->
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package cn.iocoder.yudao.module.im;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目的启动类
|
||||||
|
*
|
||||||
|
* 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
|
||||||
|
* 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
|
||||||
|
* 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@SpringBootApplication
|
||||||
|
public class ImServerApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
// 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
|
||||||
|
// 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
|
||||||
|
// 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
|
||||||
|
|
||||||
|
SpringApplication.run(ImServerApplication.class, args);
|
||||||
|
|
||||||
|
// 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
|
||||||
|
// 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
|
||||||
|
// 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package cn.iocoder.yudao.module.im.controller.admin.channel;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||||
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||||
|
import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.material.ImChannelMaterialRespVO;
|
||||||
|
import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelMaterialDO;
|
||||||
|
import cn.iocoder.yudao.module.im.service.channel.ImChannelMaterialService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
|
|
||||||
|
@Tag(name = "用户 APP - IM 频道素材")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/im/channel/material")
|
||||||
|
@Validated
|
||||||
|
public class ImChannelMaterialController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ImChannelMaterialService channelMaterialService;
|
||||||
|
|
||||||
|
@GetMapping("/get")
|
||||||
|
@Operation(summary = "获取素材详情;用于客户端点击图文卡片渲染详情页")
|
||||||
|
@Parameter(name = "id", description = "素材编号", required = true, example = "1024")
|
||||||
|
public CommonResult<ImChannelMaterialRespVO> getMaterial(@RequestParam("id") Long id) {
|
||||||
|
ImChannelMaterialDO material = channelMaterialService.validateMaterialExists(id);
|
||||||
|
return success(BeanUtils.toBean(material, ImChannelMaterialRespVO.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
package cn.iocoder.yudao.module.im.controller.admin.face;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.ListUtil;
|
||||||
|
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||||
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||||
|
import cn.iocoder.yudao.module.im.controller.admin.face.vo.pack.ImFacePackUserRespVO;
|
||||||
|
import cn.iocoder.yudao.module.im.dal.dataobject.face.ImFacePackDO;
|
||||||
|
import cn.iocoder.yudao.module.im.dal.dataobject.face.ImFacePackItemDO;
|
||||||
|
import cn.iocoder.yudao.module.im.service.face.ImFacePackItemService;
|
||||||
|
import cn.iocoder.yudao.module.im.service.face.ImFacePackService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
|
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||||
|
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
|
||||||
|
|
||||||
|
@Tag(name = "管理后台 - IM 表情包")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/im/face-pack")
|
||||||
|
@Validated
|
||||||
|
public class ImFacePackController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ImFacePackService facePackService;
|
||||||
|
@Resource
|
||||||
|
private ImFacePackItemService facePackItemService;
|
||||||
|
|
||||||
|
@GetMapping("/list")
|
||||||
|
@Operation(summary = "获得启用的表情包列表(含表情)")
|
||||||
|
public CommonResult<List<ImFacePackUserRespVO>> getFacePackList() {
|
||||||
|
// 1.1 拉所有启用表情包
|
||||||
|
List<ImFacePackDO> packs = facePackService.getEnabledFacePackList();
|
||||||
|
if (packs.isEmpty()) {
|
||||||
|
return success(ListUtil.of());
|
||||||
|
}
|
||||||
|
// 1.2 拉这些包下所有启用表情,按 packId 分组
|
||||||
|
List<ImFacePackItemDO> items = facePackItemService.getEnabledItemListByPackIds(
|
||||||
|
convertList(packs, ImFacePackDO::getId));
|
||||||
|
Map<Long, List<ImFacePackItemDO>> itemsByPackId = convertMultiMap(items, ImFacePackItemDO::getPackId);
|
||||||
|
|
||||||
|
// 2. 拼装:BeanUtils 把 pack 字段映射 + 自己塞 items
|
||||||
|
List<ImFacePackUserRespVO> result = convertList(packs, pack -> {
|
||||||
|
ImFacePackUserRespVO vo = BeanUtils.toBean(pack, ImFacePackUserRespVO.class);
|
||||||
|
vo.setItems(BeanUtils.toBean(itemsByPackId.getOrDefault(pack.getId(), ListUtil.of()), ImFacePackUserRespVO.Item.class));
|
||||||
|
return vo;
|
||||||
|
});
|
||||||
|
return success(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
package cn.iocoder.yudao.module.im.controller.admin.face;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||||
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||||
|
import cn.iocoder.yudao.module.im.controller.admin.face.vo.useritem.ImFaceUserItemRespVO;
|
||||||
|
import cn.iocoder.yudao.module.im.controller.admin.face.vo.useritem.ImFaceUserItemSaveReqVO;
|
||||||
|
import cn.iocoder.yudao.module.im.dal.dataobject.face.ImFaceUserItemDO;
|
||||||
|
import cn.iocoder.yudao.module.im.service.face.ImFaceUserItemService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
|
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
|
||||||
|
|
||||||
|
@Tag(name = "管理后台 - IM 个人表情")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/im/face-user-item")
|
||||||
|
@Validated
|
||||||
|
public class ImFaceUserItemController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ImFaceUserItemService faceUserItemService;
|
||||||
|
|
||||||
|
@GetMapping("/list")
|
||||||
|
@Operation(summary = "获得我的个人表情列表")
|
||||||
|
public CommonResult<List<ImFaceUserItemRespVO>> getFaceUserItemList() {
|
||||||
|
List<ImFaceUserItemDO> items = faceUserItemService.getFaceUserItemList(getLoginUserId());
|
||||||
|
return success(BeanUtils.toBean(items, ImFaceUserItemRespVO.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/create")
|
||||||
|
@Operation(summary = "添加个人表情")
|
||||||
|
public CommonResult<Long> createFaceUserItem(@Valid @RequestBody ImFaceUserItemSaveReqVO reqVO) {
|
||||||
|
return success(faceUserItemService.createFaceUserItem(getLoginUserId(), reqVO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/delete")
|
||||||
|
@Operation(summary = "删除个人表情")
|
||||||
|
@Parameter(name = "id", description = "编号", required = true, example = "4096")
|
||||||
|
public CommonResult<Boolean> deleteFaceUserItem(@RequestParam("id") Long id) {
|
||||||
|
faceUserItemService.deleteFaceUserItem(getLoginUserId(), id);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
package cn.iocoder.yudao.module.im.controller.admin.face.vo.pack;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Schema(description = "IM 表情包(用户端) Response VO")
|
||||||
|
@Data
|
||||||
|
public class ImFacePackUserRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "表情包名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "猫主子")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "表情包图标", example = "https://cdn.example.com/face/pack/cat.png")
|
||||||
|
private String icon;
|
||||||
|
|
||||||
|
@Schema(description = "表情列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private List<Item> items;
|
||||||
|
|
||||||
|
@Schema(description = "IM 表情包项(用户端)")
|
||||||
|
@Data
|
||||||
|
public static class Item {
|
||||||
|
|
||||||
|
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "表情图 URL", requiredMode = Schema.RequiredMode.REQUIRED,
|
||||||
|
example = "https://cdn.example.com/face/pack/cat-001.png")
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
@Schema(description = "表情名", example = "狗头")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "渲染宽度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
|
||||||
|
private Integer width;
|
||||||
|
|
||||||
|
@Schema(description = "渲染高度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
|
||||||
|
private Integer height;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package cn.iocoder.yudao.module.im.controller.admin.face.vo.useritem;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Schema(description = "IM 个人表情 Response VO")
|
||||||
|
@Data
|
||||||
|
public class ImFaceUserItemRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "表情图 URL", requiredMode = Schema.RequiredMode.REQUIRED,
|
||||||
|
example = "https://cdn.example.com/face/user/abc.gif")
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
@Schema(description = "表情名", example = "狗头")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "渲染宽度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
|
||||||
|
private Integer width;
|
||||||
|
|
||||||
|
@Schema(description = "渲染高度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
|
||||||
|
private Integer height;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package cn.iocoder.yudao.module.im.controller.admin.face.vo.useritem;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import javax.validation.constraints.Max;
|
||||||
|
import javax.validation.constraints.Min;
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import javax.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Schema(description = "IM 个人表情新增 Request VO")
|
||||||
|
@Data
|
||||||
|
public class ImFaceUserItemSaveReqVO {
|
||||||
|
|
||||||
|
@Schema(description = "表情图 URL", requiredMode = Schema.RequiredMode.REQUIRED,
|
||||||
|
example = "https://cdn.example.com/face/user/abc.gif")
|
||||||
|
@NotBlank(message = "表情图 URL 不能为空")
|
||||||
|
@Size(max = 512, message = "表情图 URL 长度不能超过 512")
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
@Schema(description = "表情名", example = "狗头")
|
||||||
|
@Size(max = 64, message = "表情名长度不能超过 64")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "渲染宽度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
|
||||||
|
@NotNull(message = "渲染宽度不能为空")
|
||||||
|
@Min(value = 1, message = "渲染宽度不能小于 1 像素")
|
||||||
|
@Max(value = 2048, message = "渲染宽度不能大于 2048 像素")
|
||||||
|
private Integer width;
|
||||||
|
|
||||||
|
@Schema(description = "渲染高度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
|
||||||
|
@NotNull(message = "渲染高度不能为空")
|
||||||
|
@Min(value = 1, message = "渲染高度不能小于 1 像素")
|
||||||
|
@Max(value = 2048, message = "渲染高度不能大于 2048 像素")
|
||||||
|
private Integer height;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
package cn.iocoder.yudao.module.im.controller.admin.friend;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||||
|
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
|
||||||
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||||
|
import cn.iocoder.yudao.framework.common.util.string.StrUtils;
|
||||||
|
import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendRespVO;
|
||||||
|
import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO;
|
||||||
|
import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
|
||||||
|
import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
|
||||||
|
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
|
||||||
|
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameters;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
|
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||||
|
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.singleton;
|
||||||
|
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
|
||||||
|
|
||||||
|
@Tag(name = "管理后台 - IM 好友")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/im/friend")
|
||||||
|
@Validated
|
||||||
|
public class ImFriendController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ImFriendService friendService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private AdminUserApi adminUserApi;
|
||||||
|
|
||||||
|
@GetMapping("/list")
|
||||||
|
@Operation(summary = "获得当前登录用户的好友列表")
|
||||||
|
public CommonResult<List<ImFriendRespVO>> getMyFriendList() {
|
||||||
|
// 含 DISABLE 历史好友:保留给前端展示「已删除好友」的历史对话信息;前端按 status 决定会话级联清理
|
||||||
|
List<ImFriendDO> friends = friendService.getFriendList(getLoginUserId());
|
||||||
|
return success(buildFriendRespVOList(friends));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/get")
|
||||||
|
@Operation(summary = "获得好友详情")
|
||||||
|
@Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048")
|
||||||
|
public CommonResult<ImFriendRespVO> getFriend(@RequestParam("friendUserId") Long friendUserId) {
|
||||||
|
ImFriendDO friend = friendService.getFriend(getLoginUserId(), friendUserId);
|
||||||
|
return success(buildFriendRespVO(friend));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/delete")
|
||||||
|
@Operation(summary = "删除好友(单向软删除)")
|
||||||
|
@Parameters({
|
||||||
|
@Parameter(description = "好友的用户编号", required = true, example = "2048"),
|
||||||
|
@Parameter(description = "是否级联清理本端相关数据(如私聊会话)")
|
||||||
|
})
|
||||||
|
public CommonResult<Boolean> deleteFriend(
|
||||||
|
@RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId,
|
||||||
|
@RequestParam(value = "clear", required = false) Boolean clear) {
|
||||||
|
friendService.deleteFriend(getLoginUserId(), friendUserId, clear);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/update")
|
||||||
|
@Operation(summary = "更新好友单边属性(备注 / 免打扰 / 联系人置顶)")
|
||||||
|
public CommonResult<Boolean> updateFriend(@Valid @RequestBody ImFriendUpdateReqVO reqVO) {
|
||||||
|
friendService.updateFriend(getLoginUserId(), reqVO);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/block")
|
||||||
|
@Operation(summary = "拉黑好友(必须先是好友;单边屏蔽对方私聊消息)")
|
||||||
|
@Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048")
|
||||||
|
public CommonResult<Boolean> blockFriend(
|
||||||
|
@RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId) {
|
||||||
|
friendService.blockFriend(getLoginUserId(), friendUserId);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/unblock")
|
||||||
|
@Operation(summary = "移出黑名单")
|
||||||
|
@Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048")
|
||||||
|
public CommonResult<Boolean> unblockFriend(
|
||||||
|
@RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId) {
|
||||||
|
friendService.unblockFriend(getLoginUserId(), friendUserId);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 私有方法:VO 组装 ==========
|
||||||
|
|
||||||
|
private List<ImFriendRespVO> buildFriendRespVOList(Collection<ImFriendDO> friends) {
|
||||||
|
if (CollUtil.isEmpty(friends)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
// 批量聚合 AdminUser 信息(昵称 / 头像),避免 N+1
|
||||||
|
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
|
||||||
|
convertList(friends, ImFriendDO::getFriendUserId));
|
||||||
|
return convertList(friends, friend -> {
|
||||||
|
ImFriendRespVO vo = BeanUtils.toBean(friend, ImFriendRespVO.class);
|
||||||
|
MapUtils.findAndThen(userMap, friend.getFriendUserId(), user ->
|
||||||
|
vo.setNickname(user.getNickname()).setAvatar(user.getAvatar()));
|
||||||
|
// 备注 / 昵称的拼音,给前端做字母分桶 + 拼音搜索
|
||||||
|
vo.setDisplayNamePinyin(StrUtils.toPinyin(vo.getDisplayName()))
|
||||||
|
.setNicknamePinyin(StrUtils.toPinyin(vo.getNickname()));
|
||||||
|
return vo;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private ImFriendRespVO buildFriendRespVO(ImFriendDO friend) {
|
||||||
|
if (friend == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return CollUtil.getFirst(buildFriendRespVOList(singleton(friend)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
package cn.iocoder.yudao.module.im.controller.admin.friend;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import cn.hutool.core.util.ObjUtil;
|
||||||
|
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||||
|
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
|
||||||
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||||
|
import cn.iocoder.yudao.module.im.controller.admin.friend.vo.request.ImFriendRequestApplyReqVO;
|
||||||
|
import cn.iocoder.yudao.module.im.controller.admin.friend.vo.request.ImFriendRequestRespVO;
|
||||||
|
import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO;
|
||||||
|
import cn.iocoder.yudao.module.im.service.friend.ImFriendRequestService;
|
||||||
|
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
|
||||||
|
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import javax.validation.constraints.Max;
|
||||||
|
import javax.validation.constraints.Min;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import javax.validation.constraints.Size;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
|
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||||
|
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
|
||||||
|
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IM 好友申请记录 Controller
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Tag(name = "管理后台 - IM 好友申请")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/im/friend-request")
|
||||||
|
@Validated
|
||||||
|
public class ImFriendRequestController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ImFriendRequestService friendRequestService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private AdminUserApi adminUserApi;
|
||||||
|
|
||||||
|
@PostMapping("/apply")
|
||||||
|
@Operation(summary = "发起好友申请")
|
||||||
|
public CommonResult<Long> applyFriend(@Valid @RequestBody ImFriendRequestApplyReqVO reqVO) {
|
||||||
|
ImFriendRequestDO request = friendRequestService.applyFriend(getLoginUserId(), reqVO);
|
||||||
|
return success(request != null ? request.getId() : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/agree")
|
||||||
|
@Operation(summary = "同意好友申请")
|
||||||
|
@Parameter(name = "id", description = "申请编号", required = true, example = "1024")
|
||||||
|
public CommonResult<Boolean> agreeFriendRequest(
|
||||||
|
@RequestParam("id") @NotNull(message = "申请编号不能为空") Long id) {
|
||||||
|
friendRequestService.agreeFriendRequest(getLoginUserId(), id);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/refuse")
|
||||||
|
@Operation(summary = "拒绝好友申请")
|
||||||
|
public CommonResult<Boolean> refuseFriendRequest(
|
||||||
|
@RequestParam("id") @NotNull(message = "申请编号不能为空") Long id,
|
||||||
|
@RequestParam(value = "handleContent", required = false)
|
||||||
|
@Size(max = 255, message = "处理理由最多 255 个字符") String handleContent) {
|
||||||
|
friendRequestService.refuseFriendRequest(getLoginUserId(), id, handleContent);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/list")
|
||||||
|
@Operation(summary = "查询「我相关」的好友申请列表(游标分页:传 maxId 加载更多)")
|
||||||
|
public CommonResult<List<ImFriendRequestRespVO>> getMyFriendRequestList(
|
||||||
|
@Parameter(description = "当前列表最旧记录的 id;首页不传")
|
||||||
|
@RequestParam(value = "maxId", required = false) Long maxId,
|
||||||
|
@Parameter(description = "单次拉取条数", required = true)
|
||||||
|
@RequestParam("limit") @Min(1) @Max(200) Integer limit) {
|
||||||
|
List<ImFriendRequestDO> list = friendRequestService.getMyFriendRequestList(getLoginUserId(), maxId, limit);
|
||||||
|
return success(buildList(list));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/get")
|
||||||
|
@Operation(summary = "按 id 单查「我相关」的申请记录(带越权过滤;WebSocket 通知到达后用)")
|
||||||
|
@Parameter(name = "id", description = "申请记录编号", required = true)
|
||||||
|
public CommonResult<ImFriendRequestRespVO> getMyFriendRequest(@RequestParam("id") Long id) {
|
||||||
|
ImFriendRequestDO request = friendRequestService.getFriendRequest(id);
|
||||||
|
// 越权过滤:fromUser / toUser 必有一方是当前用户,否则当不存在返回 null
|
||||||
|
Long currentUserId = getLoginUserId();
|
||||||
|
if (request == null || (ObjUtil.notEqual(request.getFromUserId(), currentUserId)
|
||||||
|
&& ObjUtil.notEqual(request.getToUserId(), currentUserId))) {
|
||||||
|
return success(null);
|
||||||
|
}
|
||||||
|
return success(CollUtil.getFirst(buildList(Collections.singletonList(request))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 私有方法:VO 组装 ==========
|
||||||
|
|
||||||
|
private List<ImFriendRequestRespVO> buildList(List<ImFriendRequestDO> list) {
|
||||||
|
if (CollUtil.isEmpty(list)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
// 双向 OR 列表,userIds 取 from + to 两组并集
|
||||||
|
Set<Long> userIds = convertSetByFlatMap(list,
|
||||||
|
request -> Stream.of(request.getFromUserId(), request.getToUserId()));
|
||||||
|
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
|
||||||
|
return convertList(list, request -> {
|
||||||
|
ImFriendRequestRespVO vo = BeanUtils.toBean(request, ImFriendRequestRespVO.class);
|
||||||
|
MapUtils.findAndThen(userMap, request.getFromUserId(), user ->
|
||||||
|
vo.setFromNickname(user.getNickname()).setFromAvatar(user.getAvatar()));
|
||||||
|
MapUtils.findAndThen(userMap, request.getToUserId(), user ->
|
||||||
|
vo.setToNickname(user.getNickname()).setToAvatar(user.getAvatar()));
|
||||||
|
return vo;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
package cn.iocoder.yudao.module.im.controller.admin.friend.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IM 好友 Response VO
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Schema(description = "管理后台 - IM 好友 Response VO")
|
||||||
|
@Data
|
||||||
|
public class ImFriendRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "关系记录编号", example = "1024")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "好友的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
|
||||||
|
private Long friendUserId;
|
||||||
|
|
||||||
|
@Schema(description = "是否免打扰", example = "false")
|
||||||
|
private Boolean silent;
|
||||||
|
|
||||||
|
@Schema(description = "好友展示备注(仅自己可见)", example = "老张")
|
||||||
|
private String displayName;
|
||||||
|
|
||||||
|
@Schema(description = "好友展示备注的拼音(小写无空格)", example = "laozhang")
|
||||||
|
private String displayNamePinyin;
|
||||||
|
|
||||||
|
@Schema(description = "添加来源", example = "1")
|
||||||
|
private Integer addSource; // 参见 ImFriendAddSourceEnum 枚举
|
||||||
|
|
||||||
|
@Schema(description = "是否置顶联系人", example = "false")
|
||||||
|
private Boolean pinned;
|
||||||
|
|
||||||
|
@Schema(description = "是否拉黑(仅自己可见)", example = "false")
|
||||||
|
private Boolean blocked;
|
||||||
|
|
||||||
|
@Schema(description = "好友状态", example = "0")
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
@Schema(description = "添加好友时间")
|
||||||
|
private LocalDateTime addTime;
|
||||||
|
|
||||||
|
@Schema(description = "删除好友时间")
|
||||||
|
private LocalDateTime deleteTime;
|
||||||
|
|
||||||
|
// ========== 下面是聚合字段,方便前端显示 ==========
|
||||||
|
|
||||||
|
@Schema(description = "好友昵称(实时聚合自 AdminUser)", example = "芋道")
|
||||||
|
private String nickname;
|
||||||
|
|
||||||
|
@Schema(description = "好友昵称的拼音(小写无空格)", example = "yudao")
|
||||||
|
private String nicknamePinyin;
|
||||||
|
|
||||||
|
@Schema(description = "好友头像(实时聚合自 AdminUser)")
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package cn.iocoder.yudao.module.im.controller.admin.friend.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import javax.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - IM 好友更新 Request VO")
|
||||||
|
@Data
|
||||||
|
public class ImFriendUpdateReqVO {
|
||||||
|
|
||||||
|
@Schema(description = "好友的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
|
||||||
|
@NotNull(message = "好友用户编号不能为空")
|
||||||
|
private Long friendUserId;
|
||||||
|
|
||||||
|
@Schema(description = "是否免打扰;不传表示不修改", example = "true")
|
||||||
|
private Boolean silent;
|
||||||
|
|
||||||
|
@Schema(description = "好友展示备注(仅自己可见);不传表示不修改,传空串表示清空", example = "老张")
|
||||||
|
@Size(max = 16, message = "好友备注最多 16 个字符")
|
||||||
|
private String displayName;
|
||||||
|
|
||||||
|
@Schema(description = "是否置顶联系人;不传表示不修改", example = "true")
|
||||||
|
private Boolean pinned;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package cn.iocoder.yudao.module.im.controller.admin.friend.vo.request;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.common.validation.InEnum;
|
||||||
|
import cn.iocoder.yudao.module.im.enums.friend.ImFriendAddSourceEnum;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import javax.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IM 好友申请 - 发起 Request VO
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Schema(description = "管理后台 - IM 好友申请发起 Request VO")
|
||||||
|
@Data
|
||||||
|
public class ImFriendRequestApplyReqVO {
|
||||||
|
|
||||||
|
@Schema(description = "接收方用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
|
||||||
|
@NotNull(message = "接收方用户编号不能为空")
|
||||||
|
private Long toUserId;
|
||||||
|
|
||||||
|
@Schema(description = "申请理由", example = "我是芋艿(一种食材)")
|
||||||
|
@Size(max = 255, message = "申请理由最多 255 个字符")
|
||||||
|
private String applyContent;
|
||||||
|
|
||||||
|
@Schema(description = "对接收方的备注(仅自己可见)", example = "老张")
|
||||||
|
@Size(max = 16, message = "好友备注最多 16 个字符")
|
||||||
|
private String displayName;
|
||||||
|
|
||||||
|
@Schema(description = "添加来源", example = "1")
|
||||||
|
@InEnum(ImFriendAddSourceEnum.class)
|
||||||
|
private Integer addSource;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
package cn.iocoder.yudao.module.im.controller.admin.friend.vo.request;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IM 好友申请 Response VO
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Schema(description = "管理后台 - IM 好友申请 Response VO")
|
||||||
|
@Data
|
||||||
|
public class ImFriendRequestRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "申请编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "发起方用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
|
||||||
|
private Long fromUserId;
|
||||||
|
|
||||||
|
@Schema(description = "接收方用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
|
||||||
|
private Long toUserId;
|
||||||
|
|
||||||
|
@Schema(description = "处理结果", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||||
|
private Integer handleResult; // 参见 ImFriendRequestHandleResultEnum 枚举
|
||||||
|
|
||||||
|
@Schema(description = "申请理由", example = "我是芋艿(一种食材)")
|
||||||
|
private String applyContent;
|
||||||
|
|
||||||
|
@Schema(description = "处理理由(接收方拒绝时可选填)", example = "暂不通过")
|
||||||
|
private String handleContent;
|
||||||
|
|
||||||
|
@Schema(description = "添加来源", example = "1")
|
||||||
|
private Integer addSource; // 参见 ImFriendAddSourceEnum 枚举
|
||||||
|
|
||||||
|
@Schema(description = "处理时间")
|
||||||
|
private LocalDateTime handleTime;
|
||||||
|
|
||||||
|
@Schema(description = "申请创建时间")
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
// ========== 下面是聚合字段,方便前端显示 ==========
|
||||||
|
|
||||||
|
@Schema(description = "发起方昵称(实时聚合自 AdminUser)", example = "芋道")
|
||||||
|
private String fromNickname;
|
||||||
|
|
||||||
|
@Schema(description = "发起方头像(实时聚合自 AdminUser)")
|
||||||
|
private String fromAvatar;
|
||||||
|
|
||||||
|
@Schema(description = "接收方昵称(实时聚合自 AdminUser)", example = "老张")
|
||||||
|
private String toNickname;
|
||||||
|
|
||||||
|
@Schema(description = "接收方头像(实时聚合自 AdminUser)")
|
||||||
|
private String toAvatar;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,206 @@
|
||||||
|
package cn.iocoder.yudao.module.im.controller.admin.group;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||||
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||||
|
import cn.iocoder.yudao.module.im.controller.admin.group.vo.*;
|
||||||
|
import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO;
|
||||||
|
import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO;
|
||||||
|
import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageRespVO;
|
||||||
|
import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
|
||||||
|
import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
|
||||||
|
import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
|
||||||
|
import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
|
||||||
|
import cn.iocoder.yudao.module.im.service.group.ImGroupService;
|
||||||
|
import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
|
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
|
||||||
|
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
|
||||||
|
|
||||||
|
@Tag(name = "管理后台 - 群")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/im/group")
|
||||||
|
@Validated
|
||||||
|
public class ImGroupController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ImGroupService groupService;
|
||||||
|
@Resource
|
||||||
|
private ImGroupMemberService groupMemberService;
|
||||||
|
@Resource
|
||||||
|
private ImGroupMessageService groupMessageService;
|
||||||
|
|
||||||
|
// ==================== 群的写操作 ====================
|
||||||
|
|
||||||
|
@PostMapping("/create")
|
||||||
|
@Operation(summary = "创建群")
|
||||||
|
public CommonResult<ImGroupRespVO> createGroup(@Valid @RequestBody ImGroupCreateReqVO createReqVO) {
|
||||||
|
ImGroupDO group = groupService.createGroup(createReqVO, getLoginUserId());
|
||||||
|
// 新建群必无 pinnedMessages,跳过关联回填
|
||||||
|
return success(BeanUtils.toBean(group, ImGroupRespVO.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/update")
|
||||||
|
@Operation(summary = "更新群")
|
||||||
|
public CommonResult<ImGroupRespVO> updateGroup(@Valid @RequestBody ImGroupUpdateReqVO updateReqVO) {
|
||||||
|
ImGroupDO group = groupService.updateGroup(updateReqVO, getLoginUserId());
|
||||||
|
return success(buildGroupRespVO(group, getLoginUserId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/dissolve")
|
||||||
|
@Operation(summary = "解散群")
|
||||||
|
@Parameter(name = "id", description = "群编号", required = true)
|
||||||
|
public CommonResult<Boolean> dissolveGroup(@RequestParam("id") Long id) {
|
||||||
|
groupService.dissolveGroup(id, getLoginUserId());
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 群的读操作 ====================
|
||||||
|
|
||||||
|
@GetMapping("/get")
|
||||||
|
@Operation(summary = "获得群")
|
||||||
|
@Parameter(name = "id", description = "编号", required = true, example = "1024")
|
||||||
|
public CommonResult<ImGroupRespVO> getGroup(@RequestParam("id") Long id) {
|
||||||
|
ImGroupDO group = groupService.getGroup(id);
|
||||||
|
return success(buildGroupRespVO(group, getLoginUserId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/list")
|
||||||
|
@Operation(summary = "获得当前登录用户的群列表")
|
||||||
|
public CommonResult<List<ImGroupRespVO>> getMyGroupList() {
|
||||||
|
Long loginUserId = getLoginUserId();
|
||||||
|
List<ImGroupDO> groups = groupService.getMyGroupList(loginUserId);
|
||||||
|
return success(buildGroupRespVOList(groups, loginUserId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 群成员的写操作 ====================
|
||||||
|
|
||||||
|
@PostMapping("/invite")
|
||||||
|
@Operation(summary = "邀请用户加入群")
|
||||||
|
public CommonResult<Boolean> inviteGroupMember(@Valid @RequestBody ImGroupMemberInviteReqVO inviteReqVO) {
|
||||||
|
groupService.inviteGroupMember(getLoginUserId(), inviteReqVO);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/quit")
|
||||||
|
@Operation(summary = "退出群")
|
||||||
|
@Parameter(name = "groupId", description = "群编号", required = true)
|
||||||
|
public CommonResult<Boolean> quitGroup(@RequestParam("groupId") Long groupId) {
|
||||||
|
groupService.quitGroup(groupId, getLoginUserId());
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/kicking")
|
||||||
|
@Operation(summary = "移除群成员")
|
||||||
|
public CommonResult<Boolean> removeGroupMember(@Valid @RequestBody ImGroupMemberRemoveReqVO removeReqVO) {
|
||||||
|
groupService.removeGroupMember(getLoginUserId(), removeReqVO);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/add-admin")
|
||||||
|
@Operation(summary = "添加群管理员")
|
||||||
|
public CommonResult<Boolean> addGroupAdmin(@Valid @RequestBody ImGroupAdminAddReqVO reqVO) {
|
||||||
|
groupService.addGroupAdmin(getLoginUserId(), reqVO);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/remove-admin")
|
||||||
|
@Operation(summary = "撤销群管理员")
|
||||||
|
public CommonResult<Boolean> removeGroupAdmin(@Valid @RequestBody ImGroupAdminRemoveReqVO reqVO) {
|
||||||
|
groupService.removeGroupAdmin(getLoginUserId(), reqVO);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/transfer-owner")
|
||||||
|
@Operation(summary = "转让群主")
|
||||||
|
public CommonResult<Boolean> transferGroupOwner(@Valid @RequestBody ImGroupTransferOwnerReqVO transferReqVO) {
|
||||||
|
groupService.transferGroupOwner(getLoginUserId(), transferReqVO);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 群消息置顶 ====================
|
||||||
|
|
||||||
|
@PutMapping("/pin-message")
|
||||||
|
@Operation(summary = "置顶群消息(群主 / 管理员)")
|
||||||
|
public CommonResult<Boolean> pinGroupMessage(@Valid @RequestBody ImGroupMessagePinReqVO reqVO) {
|
||||||
|
groupService.pinGroupMessage(getLoginUserId(), reqVO.getId(), reqVO.getMessageId());
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/unpin-message")
|
||||||
|
@Operation(summary = "取消置顶群消息(群主 / 管理员)")
|
||||||
|
public CommonResult<Boolean> unpinGroupMessage(@Valid @RequestBody ImGroupMessagePinReqVO reqVO) {
|
||||||
|
groupService.unpinGroupMessage(getLoginUserId(), reqVO.getId(), reqVO.getMessageId());
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 群禁言 ====================
|
||||||
|
|
||||||
|
@PutMapping("/mute-all")
|
||||||
|
@Operation(summary = "全群禁言 / 取消(群主 / 管理员)")
|
||||||
|
public CommonResult<Boolean> muteAll(@Valid @RequestBody ImGroupMuteAllReqVO reqVO) {
|
||||||
|
groupService.muteAll(getLoginUserId(), reqVO);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/mute-member")
|
||||||
|
@Operation(summary = "禁言成员")
|
||||||
|
public CommonResult<Boolean> muteMember(@Valid @RequestBody ImGroupMuteMemberReqVO reqVO) {
|
||||||
|
groupService.muteMember(getLoginUserId(), reqVO);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/cancel-mute-member")
|
||||||
|
@Operation(summary = "取消成员禁言")
|
||||||
|
public CommonResult<Boolean> cancelMuteMember(@Valid @RequestBody ImGroupCancelMuteMemberReqVO reqVO) {
|
||||||
|
groupService.cancelMuteMember(getLoginUserId(), reqVO);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单群转 VO + 关联回填 pinnedMessages(仅当登录用户是该群有效成员) */
|
||||||
|
private ImGroupRespVO buildGroupRespVO(ImGroupDO group, Long loginUserId) {
|
||||||
|
if (group == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return buildGroupRespVOList(Collections.singletonList(group), loginUserId).get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 群列表批量转 VO + 关联回填 pinnedMessages
|
||||||
|
* <p>
|
||||||
|
* 仅当登录用户是某群的有效成员时才回填该群的 pinnedMessages,避免非成员 / 已退群用户越权拿到置顶消息内容
|
||||||
|
*/
|
||||||
|
private List<ImGroupRespVO> buildGroupRespVOList(List<ImGroupDO> groups, Long loginUserId) {
|
||||||
|
if (CollUtil.isEmpty(groups)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
// 仅当前用户是有效成员的群才允许回填置顶消息
|
||||||
|
Set<Long> activeGroupIds = convertSet(
|
||||||
|
groupMemberService.getActiveGroupMemberListByUserId(loginUserId), ImGroupMemberDO::getGroupId);
|
||||||
|
Set<Long> allMessageIds = convertSetByFlatMap(groups, group -> activeGroupIds.contains(group.getId())
|
||||||
|
? CollUtil.emptyIfNull(group.getPinnedMessageIds()).stream() : Stream.empty());
|
||||||
|
Map<Long, ImGroupMessageDO> messageMap = groupMessageService.getGroupMessageMap(allMessageIds);
|
||||||
|
// 转换输出
|
||||||
|
return convertList(groups, group -> {
|
||||||
|
ImGroupRespVO vo = BeanUtils.toBean(group, ImGroupRespVO.class);
|
||||||
|
if (!activeGroupIds.contains(group.getId()) || CollUtil.isEmpty(group.getPinnedMessageIds())) {
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
// 按 pin 顺序输出,已被删除的消息(messageMap 没命中)跳过
|
||||||
|
List<ImGroupMessageDO> pinnedMesages = convertList(group.getPinnedMessageIds(), messageMap::get);
|
||||||
|
return vo.setPinnedMessages(BeanUtils.toBean(pinnedMesages, ImGroupMessageRespVO.class));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
package cn.iocoder.yudao.module.im.controller.admin.group;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import cn.hutool.core.util.ObjUtil;
|
||||||
|
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
||||||
|
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||||
|
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
|
||||||
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||||
|
import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRespVO;
|
||||||
|
import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO;
|
||||||
|
import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
|
||||||
|
import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
|
||||||
|
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
|
||||||
|
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameters;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||||
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
|
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||||
|
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
|
||||||
|
import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.GROUP_MEMBER_NOT_IN_GROUP;
|
||||||
|
|
||||||
|
@Tag(name = "管理后台 - 群成员")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/im/group-member")
|
||||||
|
@Validated
|
||||||
|
public class ImGroupMemberController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ImGroupMemberService groupMemberService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private AdminUserApi adminUserApi;
|
||||||
|
|
||||||
|
@PutMapping("/update")
|
||||||
|
@Operation(summary = "更新群成员")
|
||||||
|
public CommonResult<Boolean> updateGroupMember(@Valid @RequestBody ImGroupMemberUpdateReqVO updateReqVO) {
|
||||||
|
groupMemberService.updateGroupMember(getLoginUserId(), updateReqVO);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/get")
|
||||||
|
@Operation(summary = "获得群成员")
|
||||||
|
@Parameters({
|
||||||
|
@Parameter(name = "id", description = "编号(与 groupId + userId 二选一)", example = "1024"),
|
||||||
|
@Parameter(name = "groupId", description = "群编号(与 userId 配合查)", example = "1"),
|
||||||
|
@Parameter(name = "userId", description = "用户编号(与 groupId 配合查)", example = "100")
|
||||||
|
})
|
||||||
|
public CommonResult<ImGroupMemberRespVO> getGroupMember(@RequestParam(value = "id", required = false) Long id,
|
||||||
|
@RequestParam(value = "groupId", required = false) Long groupId,
|
||||||
|
@RequestParam(value = "userId", required = false) Long userId) {
|
||||||
|
// 1. 查询群成员
|
||||||
|
ImGroupMemberDO member;
|
||||||
|
if (id != null) {
|
||||||
|
member = groupMemberService.getGroupMember(id);
|
||||||
|
} else if (groupId != null && userId != null) {
|
||||||
|
member = groupMemberService.getGroupMember(groupId, userId);
|
||||||
|
} else {
|
||||||
|
// 避免 selectByGroupIdAndUserId 收到 null 参数走全表扫 / 抛 SQL 异常
|
||||||
|
throw new IllegalArgumentException("参数缺失:需传 id 或 (groupId, userId)");
|
||||||
|
}
|
||||||
|
if (member == null) {
|
||||||
|
return success(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 校验当前登录用户是该成员所在群的有效成员
|
||||||
|
Long loginUserId = getLoginUserId();
|
||||||
|
groupMemberService.validateMemberInGroup(member.getGroupId(), loginUserId);
|
||||||
|
|
||||||
|
// 3. 转化 VO
|
||||||
|
ImGroupMemberRespVO memberVO = BeanUtils.toBean(member, ImGroupMemberRespVO.class);
|
||||||
|
AdminUserRespDTO user = adminUserApi.getUser(member.getUserId()).getCheckedData();
|
||||||
|
if (user != null) {
|
||||||
|
memberVO.setNickname(user.getNickname()).setAvatar(user.getAvatar());
|
||||||
|
}
|
||||||
|
hidePrivateFieldsIfNotSelf(memberVO, member.getUserId(), loginUserId);
|
||||||
|
return success(memberVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/list")
|
||||||
|
@Operation(summary = "获得指定群的成员列表")
|
||||||
|
@Parameter(name = "groupId", description = "群编号", required = true, example = "1024")
|
||||||
|
public CommonResult<List<ImGroupMemberRespVO>> getGroupMemberList(@RequestParam("groupId") Long groupId) {
|
||||||
|
// 1.1 查询群成员列表(包含 DISABLE 已退群的成员,不按时间过滤)
|
||||||
|
// 说明:保留已退群成员,是为了前端展示历史消息时,仍能通过该接口拿到已退群成员的昵称 / 头像信息,避免显示为空
|
||||||
|
List<ImGroupMemberDO> members = groupMemberService.getGroupMemberListByGroupId(groupId);
|
||||||
|
// 1.2 校验当前登录用户是否为群的有效成员,非成员不可查看
|
||||||
|
Long loginUserId = getLoginUserId();
|
||||||
|
if (CollUtil.findOne(members, member -> loginUserId.equals(member.getUserId())
|
||||||
|
&& CommonStatusEnum.ENABLE.getStatus().equals(member.getStatus())) == null) {
|
||||||
|
throw exception(GROUP_MEMBER_NOT_IN_GROUP);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.批量聚合 AdminUser 信息(昵称 / 头像)
|
||||||
|
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
|
||||||
|
convertList(members, ImGroupMemberDO::getUserId));
|
||||||
|
return success(convertList(members, m -> {
|
||||||
|
ImGroupMemberRespVO vo = BeanUtils.toBean(m, ImGroupMemberRespVO.class);
|
||||||
|
MapUtils.findAndThen(userMap, m.getUserId(), user ->
|
||||||
|
vo.setNickname(user.getNickname()).setAvatar(user.getAvatar()));
|
||||||
|
hidePrivateFieldsIfNotSelf(vo, m.getUserId(), loginUserId);
|
||||||
|
return vo;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 非本人查看时,置空成员的私人设置字段(groupRemark / silent)
|
||||||
|
*/
|
||||||
|
private void hidePrivateFieldsIfNotSelf(ImGroupMemberRespVO vo, Long memberUserId, Long loginUserId) {
|
||||||
|
if (ObjUtil.notEqual(loginUserId, memberUserId)) {
|
||||||
|
vo.setGroupRemark(null).setSilent(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
package cn.iocoder.yudao.module.im.controller.admin.group;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import cn.hutool.core.util.ObjUtil;
|
||||||
|
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
||||||
|
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||||
|
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
|
||||||
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||||
|
import cn.iocoder.yudao.module.im.controller.admin.group.vo.request.ImGroupRequestApplyReqVO;
|
||||||
|
import cn.iocoder.yudao.module.im.controller.admin.group.vo.request.ImGroupRequestRespVO;
|
||||||
|
import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
|
||||||
|
import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
|
||||||
|
import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupRequestDO;
|
||||||
|
import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
|
||||||
|
import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
|
||||||
|
import cn.iocoder.yudao.module.im.service.group.ImGroupRequestService;
|
||||||
|
import cn.iocoder.yudao.module.im.service.group.ImGroupService;
|
||||||
|
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
|
||||||
|
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import javax.validation.constraints.Size;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
|
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
|
||||||
|
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IM 加群申请 Controller
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Tag(name = "管理后台 - IM 加群申请")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/im/group-request")
|
||||||
|
@Validated
|
||||||
|
public class ImGroupRequestController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ImGroupRequestService groupRequestService;
|
||||||
|
@Resource
|
||||||
|
private ImGroupService groupService;
|
||||||
|
@Resource
|
||||||
|
private ImGroupMemberService groupMemberService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private AdminUserApi adminUserApi;
|
||||||
|
|
||||||
|
@PostMapping("/apply")
|
||||||
|
@Operation(summary = "申请加群")
|
||||||
|
public CommonResult<Long> applyJoinGroup(@Valid @RequestBody ImGroupRequestApplyReqVO reqVO) {
|
||||||
|
ImGroupRequestDO request = groupRequestService.applyJoinGroup(getLoginUserId(), reqVO);
|
||||||
|
return success(request != null ? request.getId() : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/agree")
|
||||||
|
@Operation(summary = "同意加群申请(群主或管理员)")
|
||||||
|
@Parameter(name = "id", description = "申请编号", required = true, example = "1024")
|
||||||
|
public CommonResult<Boolean> agreeGroupRequest(
|
||||||
|
@RequestParam("id") @NotNull(message = "申请编号不能为空") Long id) {
|
||||||
|
groupRequestService.agreeGroupRequest(getLoginUserId(), id);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/refuse")
|
||||||
|
@Operation(summary = "拒绝加群申请(群主或管理员)")
|
||||||
|
public CommonResult<Boolean> refuseGroupRequest(
|
||||||
|
@RequestParam("id") @NotNull(message = "申请编号不能为空") Long id,
|
||||||
|
@RequestParam(value = "handleContent", required = false)
|
||||||
|
@Size(max = 255, message = "处理理由最多 255 个字符") String handleContent) {
|
||||||
|
groupRequestService.refuseGroupRequest(getLoginUserId(), id, handleContent);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/unhandled-list")
|
||||||
|
@Operation(summary = "查询「我管理的所有群」下的未处理加群申请列表(不分页);前端 store 据此派生横幅红点 + Drawer 列表")
|
||||||
|
public CommonResult<List<ImGroupRequestRespVO>> getUnhandledRequestList() {
|
||||||
|
List<ImGroupRequestDO> list = groupRequestService.getUnhandledRequestListByOwnerOrAdmin(getLoginUserId());
|
||||||
|
return success(buildVOList(list));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/list-by-group")
|
||||||
|
@Operation(summary = "查询指定群下的全部加群申请(含已处理);仅群主 / 管理员可查")
|
||||||
|
@Parameter(name = "groupId", description = "群编号", required = true, example = "1024")
|
||||||
|
public CommonResult<List<ImGroupRequestRespVO>> getGroupRequestListByGroupId(
|
||||||
|
@RequestParam("groupId") @NotNull(message = "群编号不能为空") Long groupId) {
|
||||||
|
List<ImGroupRequestDO> list = groupRequestService.getGroupRequestListByGroupId(getLoginUserId(), groupId);
|
||||||
|
return success(buildVOList(list));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/get")
|
||||||
|
@Operation(summary = "按 id 单查申请记录(带越权过滤;WebSocket 通知到达后用)")
|
||||||
|
@Parameter(name = "id", description = "申请记录编号", required = true)
|
||||||
|
public CommonResult<ImGroupRequestRespVO> getGroupRequest(@RequestParam("id") Long id) {
|
||||||
|
ImGroupRequestDO request = groupRequestService.getGroupRequest(id);
|
||||||
|
if (request == null) {
|
||||||
|
return success(null);
|
||||||
|
}
|
||||||
|
// 越权过滤:申请人 / 邀请人 / 群主 / 管理员之外,当不存在返回 null
|
||||||
|
Long currentUserId = getLoginUserId();
|
||||||
|
boolean canSee = ObjUtil.equal(request.getUserId(), currentUserId)
|
||||||
|
|| ObjUtil.equal(request.getInviterUserId(), currentUserId)
|
||||||
|
|| isGroupOwnerOrAdmin(request.getGroupId(), currentUserId);
|
||||||
|
if (!canSee) {
|
||||||
|
return success(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换并返回
|
||||||
|
return success(CollUtil.getFirst(buildVOList(Collections.singletonList(request))));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前用户是否该群的有效群主 / 管理员
|
||||||
|
*/
|
||||||
|
private boolean isGroupOwnerOrAdmin(Long groupId, Long userId) {
|
||||||
|
ImGroupMemberDO member = groupMemberService.getGroupMember(groupId, userId);
|
||||||
|
return member != null
|
||||||
|
&& !CommonStatusEnum.DISABLE.getStatus().equals(member.getStatus())
|
||||||
|
&& ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 申请记录列表批量转 VO + 关联回填用户 / 群信息 */
|
||||||
|
private List<ImGroupRequestRespVO> buildVOList(List<ImGroupRequestDO> list) {
|
||||||
|
if (CollUtil.isEmpty(list)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
// 1. 聚合 user / inviter 用户信息;convertSetByFlatMap 内部已过滤 null
|
||||||
|
Set<Long> userIds = convertSetByFlatMap(list,
|
||||||
|
request -> Stream.of(request.getUserId(), request.getInviterUserId()));
|
||||||
|
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
|
||||||
|
// 2. 聚合群信息(封禁 / 解散群也要回填,便于前端展示历史)
|
||||||
|
Set<Long> groupIds = convertSet(list, ImGroupRequestDO::getGroupId);
|
||||||
|
Map<Long, ImGroupDO> groupMap = groupService.getGroupMap(groupIds);
|
||||||
|
return convertList(list, request -> {
|
||||||
|
ImGroupRequestRespVO vo = BeanUtils.toBean(request, ImGroupRequestRespVO.class);
|
||||||
|
MapUtils.findAndThen(userMap, request.getUserId(), user ->
|
||||||
|
vo.setUserNickname(user.getNickname()).setUserAvatar(user.getAvatar()));
|
||||||
|
MapUtils.findAndThen(userMap, request.getInviterUserId(), user ->
|
||||||
|
vo.setInviterNickname(user.getNickname()).setInviterAvatar(user.getAvatar()));
|
||||||
|
MapUtils.findAndThen(groupMap, request.getGroupId(), group ->
|
||||||
|
vo.setGroupName(group.getName()).setGroupAvatar(group.getAvatar()));
|
||||||
|
return vo;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package cn.iocoder.yudao.module.im.controller.admin.group.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import javax.validation.constraints.NotEmpty;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 添加群管理员 Request VO")
|
||||||
|
@Data
|
||||||
|
public class ImGroupAdminAddReqVO {
|
||||||
|
|
||||||
|
@Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279")
|
||||||
|
@NotNull(message = "群编号不能为空")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "目标用户编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[101, 102]")
|
||||||
|
@NotEmpty(message = "目标用户编号列表不能为空")
|
||||||
|
private List<Long> userIds;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package cn.iocoder.yudao.module.im.controller.admin.group.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import javax.validation.constraints.NotEmpty;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 撤销群管理员 Request VO")
|
||||||
|
@Data
|
||||||
|
public class ImGroupAdminRemoveReqVO {
|
||||||
|
|
||||||
|
@Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279")
|
||||||
|
@NotNull(message = "群编号不能为空")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "目标用户编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[101, 102]")
|
||||||
|
@NotEmpty(message = "目标用户编号列表不能为空")
|
||||||
|
private List<Long> userIds;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package cn.iocoder.yudao.module.im.controller.admin.group.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 取消成员禁言 Request VO")
|
||||||
|
@Data
|
||||||
|
public class ImGroupCancelMuteMemberReqVO {
|
||||||
|
|
||||||
|
@Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||||
|
@NotNull(message = "群编号不能为空")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "被取消禁言的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
|
||||||
|
@NotNull(message = "用户编号不能为空")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
}
|
||||||