Compare commits

...

122 Commits

Author SHA1 Message Date
YunaiV 649362ea33 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud 2026-06-01 08:14:07 +08:00
YunaiV 10035e1709 feat(im):增加 im 的功能说明 2026-06-01 08:13:35 +08:00
YunaiV de134cf3b3 【同步】BOOT 和 CLOUD 的功能(IM) 2026-06-01 00:54:28 +08:00
YunaiV c937f30436 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	pom.xml
#	yudao-gateway/src/main/resources/application.yaml
2026-06-01 00:41:37 +08:00
YunaiV 9431722bae feat(im): 统一 WebSocket 推送依赖
- 移除 IM 对 websocket starter 的直接依赖
- 改为通过 infra WebSocketSenderApi 发送推送消息
- 同步调整已有 WebSocket 推送单测
2026-06-01 00:38:26 +08:00
YunaiV 2ba231aa55 【同步】BOOT 和 CLOUD 的功能(IM)解决启动报错问题 2026-06-01 00:22:51 +08:00
YunaiV 0c261b4e02 【同步】BOOT 和 CLOUD 的功能(IM) 2026-06-01 00:12:13 +08:00
YunaiV 08c88e0cb3 (〃'▽'〃)_v2026_04_发布:新增 WMS 仓储管理系统,完成 Vben5 IoT/MES/WMS 双端适配 2026-05-31 21:31:53 +08:00
YunaiV 0ca9eda251 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	pom.xml
#	yudao-dependencies/pom.xml
#	yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/rabbitmq/IotRabbitMQMessageBus.java
2026-05-31 21:31:31 +08:00
YunaiV 1bc987805a (〃'▽'〃)_v2026_04_发布:新增 WMS 仓储管理系统,完成 Vben5 IoT/MES/WMS 双端适配 2026-05-31 21:30:20 +08:00
YunaiV 3020c6cdb0 【同步】BOOT 和 CLOUD 的功能 2026-05-31 21:22:56 +08:00
YunaiV ffb4e8c158 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserBaseVO.java
#	yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java
2026-05-31 21:00:22 +08:00
YunaiV ff4ed31c1b 【同步】BOOT 和 CLOUD 的功能 2026-05-31 20:59:11 +08:00
YunaiV 14add8edf0 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java
#	yudao-module-member/yudao-module-member-server/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserBaseVO.java
2026-05-31 20:48:47 +08:00
YunaiV 6e492e1e6b 【同步】BOOT 和 CLOUD 的功能 2026-05-31 20:47:58 +08:00
芋道源码 7a4c1a0ba4
!250 修复MemberUser缺失的email字段; 修复反射获取MemberUserApi接口路径值.
Merge pull request !250 from egd/fix-member-api
2026-05-30 14:26:18 +00:00
芋道源码 9a319b68ff
!253 iot,MessageBus增加iotRabbitMQMessageBus
Merge pull request !253 from 灬霍霍/master
2026-05-30 14:11:18 +00:00
yanwc 931472e38d iot,MessageBus增加iotRabbitMQMessageBus 2026-05-29 14:09:39 +08:00
YunaiV 47660ed156 【同步】BOOT 和 CLOUD 的功能【兼容各种 JDK8】 2026-05-24 11:14:56 +08:00
YunaiV fbeec7d731 Merge remote-tracking branch 'origin/master' 2026-05-24 11:13:16 +08:00
YunaiV 8ad9167ddd Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud 2026-05-24 11:12:57 +08:00
YunaiV 1b060dc93f Merge remote-tracking branch 'origin/master-jdk17' into master-jdk17 2026-05-23 23:18:05 +08:00
YunaiV 4363135bf8 【同步】BOOT 和 CLOUD 的功能 2026-05-23 23:17:59 +08:00
芋道源码 6d213c6746
!251 feat(mybatis-query): 添加 likeRightIfPresent 方法支持
Merge pull request !251 from wuKong/feat/feat(mybatis-query)-增加likeRightIfPresent方法支持
2026-05-23 14:25:18 +00:00
芋道源码 b8156e679d
!252 feat:代码生成器新增 Vben5 Antdv Next 前端模板(Schema + General)
Merge pull request !252 from XuZhiqiang/master
2026-05-23 13:44:10 +00:00
YunaiV d86fe5e5ca build: 升级可解析的三方依赖版本 2026-05-23 17:08:15 +08:00
YunaiV 38ec655d10 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	yudao-dependencies/pom.xml
2026-05-23 16:50:36 +08:00
YunaiV 9782e75514 build: 升级可解析的三方依赖版本 2026-05-23 16:49:20 +08:00
YunaiV e7ac652a9c Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud 2026-05-23 16:20:59 +08:00
YunaiV 17943589a4 【同步】BOOT 和 CLOUD 的功能(框架) 2026-05-23 16:20:19 +08:00
YunaiV d67f91a63e 【同步】BOOT 和 CLOUD 的功能(WMS)【兼容各种 JDK8】 2026-05-16 18:40:16 +08:00
YunaiV f6b769fc2d Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/home/WmsHomeStatisticsServiceImpl.java
2026-05-16 18:24:54 +08:00
YunaiV 36fbb9a68b 【同步】BOOT 和 CLOUD 的功能(wms) 2026-05-16 18:24:32 +08:00
YunaiV 25a823fe82 【同步】BOOT 和 CLOUD 的功能(WMS)【兼容各种 JDK8】 2026-05-16 17:57:57 +08:00
YunaiV 3314376e59 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	pom.xml
#	yudao-gateway/src/main/resources/application.yaml
2026-05-16 17:52:38 +08:00
YunaiV 050edb2db7 fix:修复 wms 启动失败
feat:增加文档说明;
2026-05-16 17:51:08 +08:00
YunaiV 5088b8c2e2 fix(config): add import-enable option for Excel import interface 2026-05-16 17:33:15 +08:00
YunaiV 8637b2a28f feat(wms):调整 README.md 2026-05-16 15:08:55 +08:00
YunaiV 1702fc1acb feat(wms):调整 README.md 2026-05-16 14:55:40 +08:00
YunaiV d5ab0b06a7 【同步】BOOT 和 CLOUD 的功能(wms) 2026-05-16 14:38:29 +08:00
YunaiV 5cf473d48e 【同步】BOOT 和 CLOUD 的功能(wms) 2026-05-16 14:37:14 +08:00
zhiqiang.xu f194e7a3d3 fix: 修复单体启动时codegen缺少配置导致的无法启动 2026-05-16 10:40:36 +08:00
zhiqiang.xu cf7b6d9cbc feat:代码生成器新增 Vben5 Antdv Next 前端模板(Schema + General)
新增 CodegenFrontTypeEnum 枚举值 VUE3_VBEN5_ANTDV_NEXT_SCHEMA(42) 和
VUE3_VBEN5_ANTDV_NEXT_GENERAL(43),适配 Vben5 框架的 web-antdv-next 应用。

主要变更:
- CodegenFrontTypeEnum 新增 42/43 枚举值
- CodegenEngine 注册 FRONT_TEMPLATES 映射及 helper 方法
- 新增 vue3_vben5_antdv_next/schema 和 general 两套 Velocity 模板
- 模板已适配 antdv-next:包名 antdv-next、TextArea、SelectOption、TabPane
- 7 个数据库的 SQL 初始化脚本同步新增字典数据
2026-05-16 10:37:10 +08:00
YunaiV 9d1dd25bc7 【同步】BOOT 和 CLOUD 的功能(wms) 2026-05-16 06:39:43 +08:00
YunaiV 63cae8bc31 【同步】BOOT 和 CLOUD 的功能(wms) 2026-05-16 06:29:06 +08:00
YunaiV 568b0c29c0 (〃'▽'〃)_v2026_04_发布:新增代码生成器 Excel 导入,增强 IoT 场景联动与数据流转 2026-05-10 10:49:03 +08:00
YunaiV 3f07aa3cd2 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	pom.xml
#	yudao-dependencies/pom.xml
2026-05-10 10:46:58 +08:00
YunaiV 42133c7141 (〃'▽'〃)_v2026_04_发布:新增代码生成器 Excel 导入,增强 IoT 场景联动与数据流转 2026-05-10 10:38:04 +08:00
YunaiV ff2dd155a6 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud 2026-05-10 10:33:30 +08:00
YunaiV a53558cf4b feat:更新 ruoyi-vue-pro.sql 2026-05-10 10:32:57 +08:00
YunaiV 52e883d5be 【同步】BOOT 和 CLOUD 的功能 2026-05-10 10:14:41 +08:00
YunaiV 32c353c53d Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud 2026-05-10 10:11:37 +08:00
YunaiV de4abc2f5f 【同步】BOOT 和 CLOUD 的功能(mall + mes) 2026-05-10 10:11:24 +08:00
YunaiV 9db40c8b80 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java
2026-05-10 09:41:19 +08:00
YunaiV e657805544 【同步】BOOT 和 CLOUD 的功能(ai + trade) 2026-05-10 09:40:28 +08:00
wuKong b8cff89a12 feat(mybatis-query): 添加 likeRightIfPresent 方法支持
- 在 LambdaQueryWrapperX 中添加 likeRightIfPresent 方法
- 在 MPJLambdaWrapperX 中添加 likeRightIfPresent 方法
- 在 QueryWrapperX 中添加 likeRightIfPresent 方法
- 实现非空字符串条件判断逻辑
- 保持链式调用的一致性设计
2026-05-08 17:34:04 +08:00
egd 3b9ab7fa4a 维持现有设计,修复MemberUser缺失的email字段;修复反射获取MemberUserApi接口路径值. 2026-05-07 15:41:40 +08:00
YunaiV 3e8eca7b8d Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	yudao-dependencies/pom.xml
2026-05-05 11:11:26 +08:00
YunaiV 4b8346ec80 【依赖升级】Phase 4:Spring Cloud 2025.0.1 + Spring Cloud Alibaba 2025.0.0.0
升级内容:
- spring-cloud 2025.0.0 → 2025.0.1
- spring-cloud-alibaba 2023.0.3.3 → 2025.0.0.0
- 移除 nacos-discovery 的 logback-adapter 排除(BOM 管理 + 新版不再依赖)
- yudao-spring-boot-starter-rpc 引入 httpclient 4.5.14
  (Spring Cloud Alibaba 2025.0.0.0 的 Nacos 不再传递 HttpClient 4.x,WxJava 4.8.x 仍需要)
2026-05-05 11:11:03 +08:00
YunaiV e3e1b2b3d5 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud 2026-05-05 10:22:55 +08:00
YunaiV 3ba4104542 【依赖升级】Phase 2 + 3:Redisson 4.3.1 + WxJava 4.8.2
Phase 2:
- redisson 3.52.0 → 4.3.1
- redisson-spring-boot-starter 排除项从 redisson-spring-data-35 改为 redisson-spring-data-40

Phase 3:
- weixin-java 4.7.9 → 4.8.2
- httpclient5 5.1.4 → 5.5.2(WxJava 4.8.x 需要 5.4+)
- httpcore5 5.1.5 → 5.3.6(配套 httpclient5 5.5.2)
2026-05-05 10:22:21 +08:00
YunaiV 23c934f727 【依赖升级】Phase 2 + 3:Redisson 4.3.1 + WxJava 4.8.2
升级内容:
- redisson 3.52.0 → 4.3.1
- weixin-java 4.7.9 → 4.8.2
2026-05-05 10:20:20 +08:00
YunaiV 61bfcdfa00 【依赖升级】Phase 1:安全依赖版本升级
升级内容:
- druid 1.2.27 → 1.2.28
- mybatis-plus 3.5.15 → 3.5.16
- mybatis-plus-join 1.5.5 → 1.5.7
- netty 4.2.9.Final → 4.2.12.Final
- lombok 1.18.42 → 1.18.46
- hutool 5.8.42 → 5.8.44
- guava 33.5.0-jre → 33.6.0-jre
- jsoup 1.21.2 → 1.22.2
- jsch 2.27.7 → 2.28.2
- commons-net 3.12.0 → 3.13.0
- vertx 4.5.22 → 4.5.26
- californium 3.12.0 → 3.14.0
- j2mod 3.2.1 → 3.3.0
- taos 3.7.9 → 3.8.3
- awssdk 2.40.15 → 2.44.0
- alipay-sdk-java 4.40.607.ALL → 4.40.771.ALL
- opengauss-jdbc 5.1.0 → 7.0.0-RC3-og
- kingbase8 8.6.0 → 9.0.1.jre7
- jimureport 2.1.3 → 2.3.2
- jimubi 2.3.0 → 2.3.2

补充:spring.cloud.version、spring.cloud.alibaba.version 添加版本上限注释
2026-05-05 10:03:53 +08:00
YunaiV 915885c825 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	yudao-dependencies/pom.xml
#	yudao-module-report/yudao-module-report-server/pom.xml
2026-05-05 10:01:31 +08:00
YunaiV c8b85ad8a7 【依赖升级】Phase 1:安全依赖版本升级
升级内容:
- spring-boot 3.5.9 → 3.5.14
- springdoc 2.8.14 → 2.8.17
- druid 1.2.27 → 1.2.28
- mybatis-plus 3.5.15 → 3.5.16
- mybatis-plus-join 1.5.5 → 1.5.7
- netty 4.2.9.Final → 4.2.12.Final
- lombok 1.18.42 → 1.18.46
- hutool 5.8.42 → 5.8.44
- guava 33.5.0-jre → 33.6.0-jre
- jsoup 1.21.2 → 1.22.2
- jsch 2.27.7 → 2.28.2
- commons-net 3.12.0 → 3.13.0
- tika-core 3.2.3 → 3.3.0
- skywalking 9.5.0 → 9.6.0
- spring-boot-admin 3.5.6 → 3.5.8
- vertx 4.5.22 → 4.5.26
- californium 3.12.0 → 3.14.0
- j2mod 3.2.1 → 3.3.0
- taos 3.7.9 → 3.8.3
- awssdk 2.40.15 → 2.44.0
- alipay-sdk-java 4.40.607.ALL → 4.40.771.ALL
- opengauss-jdbc 5.1.0 → 7.0.0-RC3-og
- kingbase8 8.6.0 → 9.0.1.jre7
- jimureport 2.1.3 → 2.3.2(artifactId 从 jimureport-spring-boot3-starter-fastjson2 改为 jimureport-spring-boot3-starter)
- jimubi 2.3.0 → 2.3.2
2026-05-05 09:58:52 +08:00
芋道源码 63e6880a10
!248 [优化] 重构 HttpUtils.replaceUrlQuery 方法,使用 Hutool 原生 API,消除不必要的反射操作
Merge pull request !248 from lliyueling/master-jdk17
2026-05-04 07:37:54 +00:00
YunaiV 4dabfea1df Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud 2026-05-04 10:27:30 +08:00
YunaiV 11a6a049fd fix(IotDeviceMessageUtils): skip JDK built-in types in reflection to avoid internal field access 2026-05-04 10:27:09 +08:00
YunaiV d545eb5631 【同步】BOOT 和 CLOUD 的功能(MES) 2026-05-04 10:08:04 +08:00
YunaiV 1f7f85bddb Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud 2026-05-04 10:05:54 +08:00
YunaiV 8f3b6cb0aa dependencies update spring-ai from 1.1.2 to 1.1.5
dependencies update alibaba-ai from 1.1.0.0-RC2 to 1.1.2.2
2026-05-04 10:05:43 +08:00
YunaiV 6780ed6879 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertRecordService.java
#	yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertRecordServiceImpl.java
#	yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleAction.java
2026-05-03 23:48:55 +08:00
YunaiV d07426e43f refactor(tests): replace JSON params with MapUtil for better readability in IOT tests 2026-05-03 23:47:40 +08:00
YunaiV 996ac02c0b 【同步】BOOT 和 CLOUD 的功能(iot) 2026-05-03 23:25:40 +08:00
YunaiV e3a34d9067 【同步】BOOT 和 CLOUD 的功能 2026-05-03 23:13:12 +08:00
YunaiV 116b6766b3 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java
2026-05-03 23:03:42 +08:00
YunaiV 6b91b4169d 【同步】BOOT 和 CLOUD 的功能(infra) 2026-05-03 22:59:43 +08:00
YunaiV 8c7087ca2a 【同步】BOOT 和 CLOUD 的功能(bpm) 2026-05-03 22:46:59 +08:00
YunaiV 3e5e60ce96 【同步】BOOT 和 CLOUD 的功能(system) 2026-05-03 22:45:50 +08:00
YunaiV f57f0c551c 【同步】BOOT 和 CLOUD 的功能(mes) 2026-05-03 22:44:39 +08:00
YunaiV e1b3589bff fix: 修正Lock4j配置中的超时时间注释 2026-05-03 20:46:37 +08:00
YunaiV d66d1fcac0 更新 flowable 版本至 6.8.1 2026-05-03 19:36:39 +08:00
YunaiV 5fe868e096 1515 feat: 修正SpringbootAdmin监控页面在iframe中可以正常显示 2026-05-03 10:47:49 +08:00
YunaiV c3125dbc92 【修复】IoT 网关调用 biz 的设备注册 / 子设备注册 RPC URL 缺前缀,导致动态注册失败 2026-05-02 09:45:20 +08:00
lliyueling adbcc60225 test(common): 补充 HttpUtils.replaceUrlQuery 单元测试
- 新增 HttpUtilsTest 测试类
- 覆盖参数替换、新增参数、空值处理等场景
- 确保优化后的 Hutool 实现与原反射实现行为一致
2026-04-23 15:40:37 +08:00
lliyueling 2fe63be6c9 refactor(http): 优化 replaceUrlQuery 方法,使用 Hutool 原生 API
- 移除了对 Hutool `UrlQuery` 内部 `query` 字段的反射访问和强制类型转换。
- 直接使用 `UrlBuilder.getQuery().remove(key)` 链式调用,代码更简洁。
- 降低了代码与 Hutool 内部实现的耦合度,提高了代码的健壮性和可读性。
2026-04-23 14:57:36 +08:00
YunaiV d947d0463a (〃'▽'〃)_v2026_03_发布:新增 MES 制造执行系统,IoT 接入 Modbus 协议 2026-04-18 12:54:45 +08:00
YunaiV 05375287bc Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	pom.xml
#	yudao-dependencies/pom.xml
2026-04-18 12:54:16 +08:00
YunaiV f5f53d59ca (〃'▽'〃)_v2026_03_发布:新增 MES 制造执行系统,IoT 接入 Modbus 协议 2026-04-18 12:53:48 +08:00
YunaiV 838e2923bb 【同步】BOOT 和 CLOUD 的功能(MES) 2026-04-18 10:25:37 +08:00
YunaiV 83ea45911b Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/client/vo/MesMdClientSaveReqVO.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/vendor/vo/MesMdVendorSaveReqVO.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/MesProTaskController.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/tm/tool/vo/MesTmToolSaveReqVO.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/tm/tool/vo/type/MesTmToolTypeSaveReqVO.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/workrecord/MesProWorkRecordDO.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/dv/machinery/MesDvMachineryService.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/dv/machinery/MesDvMachineryServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/md/unitmeasure/MesMdUnitMeasureServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/md/workstation/MesMdWorkstationMachineServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/md/workstation/MesMdWorkstationServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/pro/card/MesProCardService.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/pro/card/MesProCardServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProductBomServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/qc/indicatorresult/MesQcIndicatorResultServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcLineServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/qc/iqc/MesQcIqcLineServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/qc/iqc/MesQcIqcServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcLineServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/qc/rqc/MesQcRqcLineServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/qc/rqc/MesQcRqcServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/qc/template/MesQcTemplateIndicatorServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/tm/tool/MesTmToolServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/tm/tool/MesTmToolTypeServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptService.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/wm/outsourceissue/MesWmOutsourceIssueDetailServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/wm/outsourcereceipt/MesWmOutsourceReceiptDetailServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/wm/packages/MesWmPackageLineServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productissue/MesWmProductIssueDetailServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesDetailServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesLineServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnsales/MesWmReturnSalesLineServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnvendor/MesWmReturnVendorDetailServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnvendor/MesWmReturnVendorLineServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/main/java/cn/iocoder/yudao/module/mes/service/wm/sn/MesWmSnServiceImpl.java
#	yudao-module-mes/yudao-module-mes-server/src/test/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceServiceImplTest.java
#	yudao-module-mes/yudao-module-mes-server/src/test/java/cn/iocoder/yudao/module/mes/service/wm/returnsales/MesWmReturnSalesLineServiceImplTest.java
2026-04-18 10:15:19 +08:00
YunaiV 81009a7082 【同步】BOOT 和 CLOUD 的功能(mes) 2026-04-18 10:14:37 +08:00
YunaiV 11ff5b4a7c 【同步】BOOT 和 CLOUD 的功能(mes) 2026-04-18 10:04:51 +08:00
YunaiV b794031b71 feat:增加 iot 模块 2026-04-12 21:02:14 +08:00
YunaiV 199799b0c9 feat(mes):增加 mes 模块 2026-04-12 16:19:13 +08:00
YunaiV 86087983a7 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud 2026-04-06 22:48:51 +08:00
YunaiV 2eb326d62d 【同步】BOOT 和 CLOUD 的功能(mes) 2026-04-06 22:48:22 +08:00
YunaiV 1e14c5c38f 【同步】BOOT 和 CLOUD 的功能(mes) 2026-04-06 22:47:52 +08:00
YunaiV d9b57e6897 【同步】BOOT 和 CLOUD 的功能(MES) 2026-04-06 22:37:11 +08:00
YunaiV dc0ca32697 【同步】BOOT 和 CLOUD 的功能(MES) 2026-04-06 22:30:24 +08:00
YunaiV 79f233149c 【同步】BOOT 和 CLOUD 的功能(MES) 2026-04-06 22:27:16 +08:00
YunaiV b38cbe9c7f Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud 2026-04-06 22:21:31 +08:00
YunaiV 9d2392a81a 【同步】BOOT 和 CLOUD 的功能(mes) 2026-04-06 22:21:25 +08:00
YunaiV 2b9a03bd93 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	pom.xml
#	yudao-gateway/src/main/resources/application.yaml
2026-04-06 21:06:27 +08:00
YunaiV 16e095c343 【同步】BOOT 和 CLOUD 的功能(mes) 2026-04-06 21:01:44 +08:00
YunaiV 804d3eaaeb 【同步】BOOT 和 CLOUD 的功能 2026-04-06 20:15:50 +08:00
YunaiV 8937853307 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud 2026-03-28 16:17:38 +08:00
YunaiV 9ffec01fa0 refactor(iot): update import paths for IotDeviceStateEnum to reflect new package structure 2026-03-28 16:17:16 +08:00
YunaiV 3f599a623a 【同步】BOOT 和 CLOUD 的功能 2026-03-08 10:18:09 +08:00
YunaiV bb3f1954ee Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java
#	yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/app/tenant/AppTenantController.java
2026-03-08 10:14:33 +08:00
YunaiV 34d74b378e 【同步】BOOT 和 CLOUD 的功能 2026-03-08 10:14:09 +08:00
芋道源码 53d3146ad9
!243 fix(trade): 修复订单项价格计算逻辑
Merge pull request !243 from irongroup/master-jdk17-sync
2026-03-08 01:51:21 +00:00
tqliang a61ecaa019 fix(trade): 修复售后日志记录中的错误引用和排序问题
- 修正了AfterSaleLogAspect中对operateType的错误引用
- 将AfterSaleLogMapper中的排序字段从createTime改为id以确保正确顺序
- 确保售后操作日志能够正确显示和按预期排序
2026-03-05 11:19:37 +08:00
tqliang f969670fd3 fix(trade): 修复订单项价格计算逻辑
- 将价格计算从使用原始价格改为使用支付价格
- 确保价格分配计算的准确性
- 解决因价格比例分配可能导致的计算误差问题
2026-02-27 08:51:45 +08:00
YunaiV 1fca0acc92 【同步】BOOT 和 CLOUD 的功能(IoT) 2026-02-14 16:52:20 +08:00
YunaiV 6ca2c97849 【同步】BOOT 和 CLOUD 的功能(IoT) 2026-02-14 16:41:18 +08:00
YunaiV 71393eed21 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/config/IotMessageBusProperties.java
#	yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java
#	yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java
#	yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/AbstractIotProtocolDownstreamSubscriber.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpDownstreamSubscriber.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpDownstreamSubscriber.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpUpstreamProtocol.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketDownstreamSubscriber.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketUpstreamProtocol.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java
#	yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java
#	yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotSceneRuleMessageSubscriber.java
#	yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java
2026-02-14 16:36:52 +08:00
YunaiV 92eda45afd 【同步】BOOT 和 CLOUD 的功能(IoT) 2026-02-14 16:35:48 +08:00
YunaiV 2d4251eda7 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud into master-jdk17 2026-02-14 16:07:31 +08:00
YunaiV 06586b85f6 【同步】BOOT 和 CLOUD 的功能 2026-02-14 16:07:21 +08:00
芋道源码 7dc6b24e66
!241 feat(trade): 添加快递轨迹时间字段反序列化配置
Merge pull request !241 from wuKong/feat/feat(trade)-添加快递轨迹时间字段反序列化配置
2026-01-30 10:14:22 +00:00
wuKong 4052c5c4d0 feat(trade): 添加快递轨迹时间字段反序列化配置
- 引入 LocalDateTimeDeserializer 处理轨迹时间字段
- 配置 time 字段使用自定义反序列化器
- 确保快递轨迹时间格式正确解析
2026-01-30 14:55:46 +08:00
3015 changed files with 234580 additions and 21501 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -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-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 等功能
* 【精简版】只包括系统功能、基础设施功能不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP 等功能
* 【完整版】包括系统功能、基础设施、会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、WMS、MES、IM 即时通讯、AI 大模型、IoT 物联网等功能
* 【精简版】只包括系统功能、基础设施功能不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、WMS、MES、IM 即时通讯、AI 大模型、IoT 物联网等功能
可参考 [《迁移文档》](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 系统、商城系统、微信公众号、AI 大模型
* 业务系统(按需):Mall 电子商城、OA 办公自动化、ERP 企业资源计划系统、WMS 仓库管理系统、CRM 客户关系管理、CMS 内容管理系统、MES 执行制造系统、AI 大模型平台、IoT 物联网系统、IM 即时通讯系统、Mobile 手机移动端、Report 数据大屏
> 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。
>
@ -273,12 +273,28 @@
![功能图](/.image/common/erp-feature.png)
### WMS 系统
演示地址:<https://cloud.iocoder.cn/wms-preview/>
![功能图](/.image/common/wms-feature.png)
![功能图](/.image/common/wms-preview.png)
### CRM 系统
演示地址:<https://cloud.iocoder.cn/crm-preview/>
![功能图](/.image/common/crm-feature.png)
### MES 系统
演示地址:<https://cloud.iocoder.cn/mes-preview/>
![功能图](/.image/common/mes-feature.png)
![功能图](/.image/common/mes-preview.png)
### AI 大模型
演示地址:<https://cloud.iocoder.cn/ai-preview/>
@ -287,6 +303,27 @@
![功能图](/.image/common/ai-preview.gif)
### IoT 物联网
演示地址:<https://cloud.iocoder.cn/iot/build>
![功能图](/.image/common/iot-feature.png)
![预览图](/.image/common/iot-preview.png)
### IM 即时通讯
演示地址Cloud<https://cloud.iocoder.cn/im-preview/>
演示地址Vue3 + Element Plus<http://dashboard-vue3.yudao.iocoder.cn>
![功能图](/.image/common/im-feature.png)
| 聊天界面 | 聊天管理 |
| --- | --- |
| ![聊天界面](/.image/common/im-preview-home.png) | ![聊天管理](/.image/common/im-preview-manager.png) |
## 🐨 技术栈
### 微服务
@ -304,7 +341,11 @@
| `yudao-module-mall` | 商城系统的 Module 模块 |
| `yudao-module-erp` | ERP 系统的 Module 模块 |
| `yudao-module-crm` | CRM 系统的 Module 模块 |
| `yudao-module-mes` | MES 系统的 Module 模块 |
| `yudao-module-wms` | WMS 系统的 Module 模块 |
| `yudao-module-im` | IM 即时通讯的 Module 模块 |
| `yudao-module-ai` | AI 大模型的 Module 模块 |
| `yudao-module-iot` | IoT 物联网的 Module 模块 |
| `yudao-module-mp` | 微信公众号的 Module 模块 |
| `yudao-module-report` | 大屏报表 Module 模块 |

View File

@ -24,9 +24,12 @@
<module>yudao-module-mall</module>
<module>yudao-module-erp</module>
<module>yudao-module-crm</module>
<module>yudao-module-iot</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/ -->
<!-- <module>yudao-module-ai</module>-->
<module>yudao-module-iot</module>
</modules>
<name>${project.artifactId}</name>
@ -34,7 +37,7 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>2026.01-jdk8-SNAPSHOT</revision>
<revision>2026.05-jdk8-SNAPSHOT</revision>
<!-- Maven 相关 -->
<java.version>1.8</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
@ -43,7 +46,7 @@
<maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version>
<flatten-maven-plugin.version>1.7.2</flatten-maven-plugin.version>
<!-- 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>
<mapstruct.version>1.6.3</mapstruct.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -62,6 +62,10 @@ def load_and_clean(sql_file: str) -> str:
content = open(sql_file, encoding="utf-8").read()
for replace_pair in REPLACE_PAIR_LIST:
content = content.replace(*replace_pair)
# 移除所有 CHARACTER SET / COLLATE 变体 (utf8mb3、utf8 等)
content = re.sub(r" CHARACTER SET \w+ COLLATE \w+", "", content)
content = re.sub(r" CHARACTER SET \w+", "", content)
content = re.sub(r" COLLATE \w+", "", content)
# 移除索引字段的前缀长度定义,例如: `name`(32) -> `name`
# 移除索引定义上的 USING BTREE COMMENT 部分
# 相关 issuehttps://t.zsxq.com/96IFc 、https://t.zsxq.com/rC3A3
@ -73,11 +77,18 @@ def load_and_clean(sql_file: str) -> str:
class Convertor(ABC):
# 不同数据库的关键字不完全一致;子类按需声明需要转义的列名。
reserved_column_names = set()
def __init__(self, src: str, db_type) -> None:
self.src = src
self.db_type = db_type
self.content = load_and_clean(self.src)
self.table_script_list = re.findall(r"CREATE TABLE [^;]*;", self.content)
# original_content 保留原始 COMMENT 信息,用于注释提取
self.original_content = open(src, encoding="utf-8").read()
# 剥离列级 COMMENT 以避免 COMMENT 值内的分号截断 CREATE TABLE 正则
content_no_comment = re.sub(r" COMMENT '(?:[^'\\]|\\.)*'", "", self.content)
self.table_script_list = re.findall(r"CREATE TABLE [^;]*;", content_no_comment)
@abstractmethod
def translate_type(self, type: str, size: Optional[Union[int, Tuple[int]]]) -> str:
@ -171,6 +182,31 @@ class Convertor(ABC):
"""
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
def inserts(table_name: str, script_content: str) -> Generator:
PREFIX = f"INSERT INTO `{table_name}`"
@ -182,7 +218,8 @@ class Convertor(ABC):
head = head.strip().replace("`", "").lower()
tail = tail.strip().replace(r"\"", '"')
# tail = tail.replace("b'0'", "'0'").replace("b'1'", "'1'")
yield f"INSERT INTO {table_name.lower()} {head} VALUES {tail}"
col_part = f" {head}" if head else ""
yield f"INSERT INTO {table_name.lower()}{col_part} VALUES {tail}"
@staticmethod
def index(ddl: Dict) -> Generator:
@ -195,18 +232,55 @@ class Convertor(ABC):
Generator[str]: create index 语句
"""
def generate_columns(columns):
keys = [
f"{col['name'].lower()}{' ' + col['order'].lower() if col['order'] != 'ASC' else ''}"
for col in columns[0]
]
return ", ".join(keys)
for no, index in enumerate(ddl["index"], 1):
columns = generate_columns(index["columns"])
for no, index in enumerate(ddl.get("index", []), 1):
columns = ", ".join(Convertor.index_columns(index.get("columns", [])))
if not columns:
continue
table_name = ddl["table_name"].lower()
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
def unique_index(ddl: Dict) -> Generator:
if "constraints" in ddl and "uniques" in ddl["constraints"]:
@ -214,7 +288,9 @@ class Convertor(ABC):
for uk in uk_list:
table_name = ddl["table_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
@staticmethod
@ -227,7 +303,8 @@ class Convertor(ABC):
yield field, comment_string
def table_comment(self, table_sql: str) -> str:
match = re.search(r"COMMENT \='([^']+)';", table_sql)
# 兼容 COMMENT='xxx' / COMMENT = 'xxx' 等等号两侧空格变体;允许 COMMENT 内含转义单引号
match = re.search(r"COMMENT\s*=\s*'((?:[^'\\]|\\.)*)'", table_sql)
return match.group(1) if match else None
def print(self):
@ -251,7 +328,9 @@ class Convertor(ABC):
error_scripts = []
for table_sql in self.table_script_list:
ddl = DDLParser(table_sql.replace("`", "")).run()
# 剥离 COMMENT 子句避免 DDLParser 无法处理中文等特殊字符
table_sql_for_parse = re.sub(r"\s+COMMENT\s+'[^']*'", "", table_sql)
ddl = DDLParser(table_sql_for_parse.replace("`", "")).run()
# 如果parse失败, 需要跟进
if len(ddl) == 0:
@ -266,17 +345,23 @@ class Convertor(ABC):
continue
# 解析注释
# 从原始 SQL 提取注释(支持中文等 DDLParser 不能处理的内容)
# 按表名定位完整建表段(以「换行 + )」起始的 ENGINE 行作为终止),避免被列 COMMENT 内的分号截断
orig_match = re.search(
rf"CREATE TABLE\s+`{re.escape(table_name)}`[\s\S]*?\n\)[^;]*;",
self.original_content,
flags=re.IGNORECASE,
)
orig_table_sql = orig_match.group() if orig_match else table_sql
comments_dict = dict(Convertor.filed_comments(orig_table_sql))
for column in table_ddl["columns"]:
column["comment"] = bytes(column["comment"], "utf-8").decode(
r"unicode_escape"
)[1:-1]
table_ddl["comment"] = bytes(table_ddl["comment"], "utf-8").decode(
r"unicode_escape"
)[1:-1]
column["comment"] = comments_dict.get(column["name"], "")
table_ddl["comment"] = self.table_comment(orig_table_sql) or ""
# 为每个表生成个6个基本部分
create = self.gen_create(table_ddl)
pk = self.gen_pk(table_name)
has_id = any(col["name"].lower() == "id" for col in table_ddl["columns"])
pk = self.gen_pk(table_name) if has_id else ""
uk = self.gen_uk(table_ddl)
index = self.gen_index(table_ddl)
comment = self.gen_comment(table_ddl)
@ -320,25 +405,31 @@ class PostgreSQLConvertor(Convertor):
if type == "varchar":
return f"varchar({size})"
if type in ("int", "int unsigned"):
if type in ("int", "int unsigned", "int unsigned zerofill"):
return "int4"
if type in ("bigint", "bigint unsigned"):
return "int8"
if type == "datetime":
if type in ("tinyint", "smallint", "tinyint unsigned"):
return "int2"
if type in ("datetime", "timestamp null"):
return "timestamp"
if type == "date":
return "date"
if type == "json":
return "jsonb"
if type == "double":
return "double precision"
if type == "timestamp":
return f"timestamp({size})"
return f"timestamp({size})" if size else "timestamp"
if type == "bit":
return "bool"
if type in ("tinyint", "smallint"):
return "int2"
if type in ("text", "longtext"):
return "text"
if type in ("blob", "mediumblob"):
if type in ("blob", "mediumblob", "longblob"):
return "bytea"
if type == "decimal":
return (
f"numeric({','.join(str(s) for s in size)})" if len(size) else "numeric"
f"numeric({','.join(str(s) for s in size)})" if size and len(size) else "numeric"
)
def gen_create(self, ddl: Dict) -> str:
@ -351,9 +442,13 @@ class PostgreSQLConvertor(Convertor):
type = col["type"].lower()
full_type = self.translate_type(type, col["size"])
if full_type is None:
raise NotImplementedError(
f"未支持的类型: '{col['type']}' (列: {name}, 表: {ddl['table_name']})"
)
nullable = "NULL" if col["nullable"] else "NOT NULL"
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()
columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]]
@ -378,7 +473,7 @@ CREATE TABLE {table_name} (
for column in table_ddl["columns"]:
table_comment = column["comment"]
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"
)
@ -407,6 +502,9 @@ CREATE TABLE {table_name} (
"""生成 insert 语句,以及根据最后的 insert id+1 生成 Sequence"""
inserts = list(Convertor.inserts(table_name, self.content))
inserts = [self.escape_insert_columns(s) for s in inserts]
# 转换 MySQL 字符串转义为 PostgreSQL 格式:\\ -> \\' -> ''
inserts = [re.sub(r"\\\\|\\'", lambda m: "\\" if m.group() == "\\\\" else "''", s) for s in inserts]
## 生成 insert 脚本
script = ""
last_id = 0
@ -452,6 +550,8 @@ INSERT INTO dual VALUES (1);
class OracleConvertor(Convertor):
reserved_column_names = {"level", "size"}
def __init__(self, src):
super().__init__(src, "Oracle")
@ -496,10 +596,8 @@ class OracleConvertor(Convertor):
# Oracle的 INSERT '' 不能通过NOT NULL校验因此对文字类型字段覆写为 NULL
nullable = "NULL" if type in ("varchar", "text", "longtext") else nullable
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 之前
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()
columns = [f"{generate_column(col).strip()}" for col in ddl["columns"]]
@ -524,7 +622,7 @@ CREATE TABLE {table_name} (
for column in table_ddl["columns"]:
table_comment = column["comment"]
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"
)
@ -556,6 +654,7 @@ CREATE TABLE {table_name} (
"""拷贝 INSERT 语句"""
inserts = []
for insert_script in Convertor.inserts(table_name, self.content):
insert_script = self.escape_insert_columns(insert_script)
# 对日期数据添加 TO_DATE 转换
insert_script = re.sub(
r"('\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}')",
@ -877,6 +976,8 @@ SET IDENTITY_INSERT {table_name.lower()} OFF;
class KingbaseConvertor(PostgreSQLConvertor):
reserved_column_names = {"level"}
def __init__(self, src):
super().__init__(src)
self.db_type = "Kingbase"
@ -895,7 +996,7 @@ class KingbaseConvertor(PostgreSQLConvertor):
if full_type == "text":
nullable = "NULL"
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()
columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]]
@ -915,6 +1016,8 @@ CREATE TABLE {table_name} (
class OpengaussConvertor(KingbaseConvertor):
reserved_column_names = set()
def __init__(self, src):
super().__init__(src)
self.db_type = "OpenGauss"

View File

@ -14,30 +14,30 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>2026.01-jdk8-SNAPSHOT</revision>
<revision>2026.05-jdk8-SNAPSHOT</revision>
<flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
<!-- 统一依赖管理 -->
<spring.framework.version>5.3.39</spring.framework.version>
<spring.security.version>5.8.16</spring.security.version>
<spring.boot.version>2.7.18</spring.boot.version>
<spring.cloud.version>2021.0.9</spring.cloud.version>
<spring.cloud.alibaba.version>2021.0.6.2</spring.cloud.alibaba.version>
<spring.cloud.version>2021.0.9</spring.cloud.version> <!-- Spring Boot 2.X 最多使用 2021.0.9 版本 -->
<spring.cloud.alibaba.version>2021.0.6.2</spring.cloud.alibaba.version> <!-- Spring Boot 2.X 最多使用 2021.0.6.2 版本 -->
<!-- Web 相关 -->
<servlet.versoin>2.5</servlet.versoin>
<springdoc.version>1.8.0</springdoc.version>
<knife4j.version>4.5.0</knife4j.version>
<!-- DB 相关 -->
<druid.version>1.2.27</druid.version>
<druid.version>1.2.28</druid.version>
<mybatis.version>3.5.19</mybatis.version>
<mybatis-plus.version>3.5.15</mybatis-plus.version>
<mybatis-plus-join.version>1.5.5</mybatis-plus-join.version>
<mybatis-plus.version>3.5.16</mybatis-plus.version>
<mybatis-plus-join.version>1.5.7</mybatis-plus-join.version>
<dynamic-datasource.version>4.5.0</dynamic-datasource.version>
<easy-trans.version>3.0.6</easy-trans.version>
<redisson.version>3.52.0</redisson.version>
<redisson.version>4.4.0</redisson.version>
<dm8.jdbc.version>8.1.3.140</dm8.jdbc.version>
<kingbase.jdbc.version>8.6.0</kingbase.jdbc.version>
<opengauss.jdbc.version>5.1.0</opengauss.jdbc.version>
<taos.version>3.7.9</taos.version>
<kingbase.jdbc.version>9.0.1.jre7</kingbase.jdbc.version>
<opengauss.jdbc.version>7.0.0-RC3-og</opengauss.jdbc.version>
<taos.version>3.8.3</taos.version>
<!-- 消息队列 -->
<rocketmq-spring.version>2.3.5</rocketmq-spring.version>
<!-- RPC 相关 -->
@ -55,38 +55,44 @@
<jedis-mock.version>1.1.12</jedis-mock.version>
<mockito-inline.version>4.11.0</mockito-inline.version>
<!-- Bpm 工作流相关 -->
<flowable.version>6.8.0</flowable.version>
<flowable.version>6.8.1</flowable.version>
<!-- 工具类相关 -->
<anji-plus-captcha.version>1.4.0</anji-plus-captcha.version>
<jsoup.version>1.21.2</jsoup.version>
<lombok.version>1.18.42</lombok.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>
<mapstruct.version>1.6.3</mapstruct.version>
<hutool-5.version>5.8.42</hutool-5.version>
<hutool-5.version>5.8.44</hutool-5.version>
<fastexcel.version>1.3.0</fastexcel.version>
<velocity.version>2.4</velocity.version> <!-- JDK8 不能从 2.4 升级到 2.4.1,会报包不存在!!!! -->
<fastjson.version>1.2.83</fastjson.version>
<guava.version>33.5.0-jre</guava.version>
<guava.version>33.6.0-jre</guava.version>
<transmittable-thread-local.version>2.14.5</transmittable-thread-local.version>
<commons-net.version>3.12.0</commons-net.version>
<commons-net.version>3.13.0</commons-net.version>
<commons-lang3.version>3.20.0</commons-lang3.version>
<jsch.version>2.27.7</jsch.version>
<jsch.version>2.28.2</jsch.version>
<tika-core.version>2.9.3</tika-core.version> <!-- JDK8 不能从 2.9.3 升级到 3.X会报 JDK8 不支持 -->
<ip2region.version>2.7.0</ip2region.version>
<bizlog-sdk.version>3.0.6</bizlog-sdk.version>
<reflections.version>0.10.2</reflections.version>
<netty.version>4.2.9.Final</netty.version>
<netty.version>4.2.14.Final</netty.version>
<mqtt.version>1.2.5</mqtt.version>
<vertx.version>4.5.22</vertx.version>
<vertx.version>4.5.26</vertx.version>
<okhttp.version>4.12.0</okhttp.version>
<californium.version>3.12.0</californium.version>
<californium.version>3.14.0</californium.version>
<j2mod.version>3.3.0</j2mod.version>
<httpclient5.version>5.5.2</httpclient5.version> <!-- WxJava 4.8.x 需要 HttpClient5 5.4+Spring Boot 2.7 默认 5.1.4 不兼容 -->
<httpcore5.version>5.3.6</httpcore5.version> <!-- 配套 httpclient5 5.5.2Spring Boot 2.7 默认 5.1.5 不兼容 -->
<!-- 三方云服务相关 -->
<awssdk.version>2.40.15</awssdk.version>
<awssdk.version>2.44.0</awssdk.version>
<justauth.version>1.16.7</justauth.version>
<justauth-starter.version>1.4.0</justauth-starter.version>
<jimureport.version>2.1.3</jimureport.version>
<jimubi.version>2.3.0</jimubi.version>
<weixin-java.version>4.7.9-20251224.161447</weixin-java.version>
<alipay-sdk-java.version>4.40.607.ALL</alipay-sdk-java.version>
<jimureport.version>2.3.4</jimureport.version>
<jimubi.version>2.3.2</jimubi.version>
<weixin-java.version>4.8.2-20260501.180637</weixin-java.version>
<bouncycastle.version>1.80</bouncycastle.version>
<alipay-sdk-java.version>4.40.806.ALL</alipay-sdk-java.version>
<!-- 专属于 JDK8 安全漏洞升级 -->
<logback.version>1.2.13</logback.version> <!-- 无法使用 1.3.X 版本,启动会报错 -->
</properties>
@ -307,7 +313,7 @@
<exclusion>
<groupId>org.redisson</groupId>
<!-- 使用 redisson-spring-data-27 替代,解决 Tuple NoClassDefFoundError 报错 -->
<artifactId>redisson-spring-data-35</artifactId>
<artifactId>redisson-spring-data-40</artifactId> <!-- Redisson 4.x 默认依赖 spring-data-40排除后使用 spring-data-27 适配 Spring Boot 2.7 -->
</exclusion>
</exclusions>
</dependency>
@ -621,6 +627,18 @@
<version>${jsoup.version}</version>
</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>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
@ -671,6 +689,30 @@
<version>${californium.version}</version>
</dependency>
<!-- Modbus 相关 -->
<dependency>
<groupId>com.ghgande</groupId>
<artifactId>j2mod</artifactId>
<version>${j2mod.version}</version>
</dependency>
<!-- WxJava 4.8.x 需要 HttpClient5 5.4+,覆盖 Spring Boot 2.7 默认的 5.1.4 -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>${httpclient5.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5</artifactId>
<version>${httpcore5.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5-h2</artifactId>
<version>${httpcore5.version}</version>
</dependency>
<!-- 三方云服务相关 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
@ -708,6 +750,24 @@
</exclusions>
</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>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>

View File

@ -53,8 +53,8 @@
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有工具类需要使用到 -->
</dependency>
@ -125,8 +125,8 @@
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<scope>provided</scope> <!-- 设置为 provided主要是 PageParam 使用到 -->
</dependency>

View File

@ -124,6 +124,22 @@ public class CollectionUtils {
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) {
if (CollUtil.isEmpty(from)) {
return new HashMap<>();
@ -349,4 +365,37 @@ public class CollectionUtils {
return (LinkedHashSet<T>) toCollection(LinkedHashSet.class, elementType, value);
}
}
public static boolean dfs(Long node, Map<Long, Set<Long>> graph) {
return dfs(node, graph, new HashSet<>(), new HashSet<>());
}
private static boolean dfs(Long node, Map<Long, Set<Long>> graph, Set<Long> visited, Set<Long> inStack) {
if (inStack.contains(node)) {
return true;
}
if (visited.contains(node)) {
return false;
}
visited.add(node);
inStack.add(node);
Set<Long> neighbors = graph.getOrDefault(node, Collections.emptySet());
for (Long neighbor : neighbors) {
if (dfs(neighbor, graph, visited, inStack)) {
return true;
}
}
inStack.remove(node);
return false;
}
/**
* head tail Listhead 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;
}
}

View File

@ -236,6 +236,18 @@ public class LocalDateTimeUtils {
return LocalDateTime.now().with(TemporalAdjusters.firstDayOfYear()).with(LocalTime.MIN);
}
/**
* N 0
*/
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,
LocalDateTime endTime,
Integer interval) {
@ -302,6 +314,21 @@ public class LocalDateTimeUtils {
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;
}
/**
*
*
@ -335,6 +362,27 @@ public class LocalDateTimeUtils {
}
}
/**
*
*
* @param date
* @return
*/
public static LocalDate getQuarterStart(LocalDate date) {
Month firstMonthOfQuarter = date.getMonth().firstMonthOfQuarter();
return LocalDate.of(date.getYear(), firstMonthOfQuarter, 1);
}
/**
*
*
* @param date
* @return
*/
public static LocalDate getWeekStart(LocalDate date) {
return date.with(DayOfWeek.MONDAY);
}
/**
* {@link LocalDateTime} Unix 1970-01-01T00:00:00Z
*

View File

@ -1,9 +1,7 @@
package cn.iocoder.yudao.framework.common.util.http;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.map.TableMap;
import cn.hutool.core.net.url.UrlBuilder;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
@ -39,8 +37,10 @@ public class HttpUtils {
}
/**
* URL
* URL query parameter
* + query parameter URL path
*
* @see #decodeUrlPath(String)
* @param value
* @return
*/
@ -49,14 +49,25 @@ public class HttpUtils {
return URLDecoder.decode(value, StandardCharsets.UTF_8.name());
}
@SuppressWarnings("unchecked")
/**
* URL
* {@link #decodeUtf8(String)} + +
* URL path
*
* @param path URL
* @return
*/
@SneakyThrows
public static String decodeUrlPath(String path) {
// 先将 + 替换为 %2B避免被 URLDecoder 解码为空格
String encoded = path.replace("+", "%2B");
return URLDecoder.decode(encoded, StandardCharsets.UTF_8.name());
}
public static String replaceUrlQuery(String url, String key, String value) {
UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset());
// 先移除
TableMap<CharSequence, CharSequence> query = (TableMap<CharSequence, CharSequence>)
ReflectUtil.getFieldValue(builder.getQuery(), "query");
query.remove(key);
// 后添加
// 先移除;再添加
builder.getQuery().remove(key);
builder.addQuery(key, value);
return builder.build();
}
@ -193,4 +204,11 @@ public class HttpUtils {
}
}
/**
* WebSocket URL HTTP URLws:// → http://wss:// → https://;其它格式原样保留
*/
public static String wsUrlToHttp(String url) {
return StrUtil.startWithIgnoreCase(url, "ws") ? "http" + url.substring(2) : url;
}
}

View File

@ -22,6 +22,7 @@ import java.lang.reflect.Type;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* JSON
@ -173,6 +174,38 @@ public class JsonUtils {
}
}
/**
* JSON Map null
*/
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
*
* @param text
* @param clazz
* @return
*/
public static <T> T parseObjectQuietly(String text, Class<T> clazz) {
if (StrUtil.isEmpty(text)) {
return null;
}
try {
return objectMapper.readValue(text, clazz);
} catch (IOException e) {
return null;
}
}
public static <T> List<T> parseArray(String text, Class<T> clazz) {
if (StrUtil.isEmpty(text)) {
return new ArrayList<>();
@ -217,6 +250,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) {
return JSONUtil.isTypeJSON(text);
}

View File

@ -60,6 +60,11 @@ public class ObjectUtils {
return Arrays.asList(array).contains(obj);
}
@SafeVarargs
public static <T> boolean notEqualsAny(T obj, T... array) {
return !Arrays.asList(array).contains(obj);
}
public static boolean isNotAllEmpty(Object... objs) {
return !ObjectUtil.isAllEmpty(objs);
}

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.common.util.string;
import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.pinyin.PinyinUtil;
import org.aspectj.lang.JoinPoint;
import java.util.Arrays;
@ -78,6 +79,16 @@ public class StrUtils {
.collect(Collectors.joining("\n"));
}
/**
* 便 / /
*/
public static String toPinyin(String str) {
if (StrUtil.isBlank(str)) {
return null;
}
return PinyinUtil.getPinyin(str);
}
/**
*
*

View File

@ -0,0 +1,52 @@
package cn.iocoder.yudao.framework.common.util.http;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* {@link HttpUtils}
*/
public class HttpUtilsTest {
@Test
public void testReplaceUrlQuery_replace() {
// 准备参数
String url = "https://www.iocoder.cn/path?a=1&b=2";
// 调用
String result = HttpUtils.replaceUrlQuery(url, "a", "3");
// 断言:被替换的 key 会移到末尾,原顺序的其它参数保留
assertEquals("https://www.iocoder.cn/path?b=2&a=3", result);
}
@Test
public void testReplaceUrlQuery_add() {
// 准备参数
String url = "https://www.iocoder.cn/path?a=1";
// 调用
String result = HttpUtils.replaceUrlQuery(url, "b", "2");
// 断言
assertEquals("https://www.iocoder.cn/path?a=1&b=2", result);
}
@Test
public void testReplaceUrlQuery_noQuery() {
// 准备参数:原 URL 没有 query
String url = "https://www.iocoder.cn/path";
// 调用
String result = HttpUtils.replaceUrlQuery(url, "a", "1");
// 断言
assertEquals("https://www.iocoder.cn/path?a=1", result);
}
@Test
public void testReplaceUrlQuery_emptyValue() {
// 准备参数value 为空字符串
String url = "https://www.iocoder.cn/path?a=1";
// 调用
String result = HttpUtils.replaceUrlQuery(url, "a", "");
// 断言:保留 keyvalue 为空
assertEquals("https://www.iocoder.cn/path?a=", result);
}
}

View File

@ -8,6 +8,7 @@ import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.framework.ip.core.Area;
import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
import lombok.NonNull;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
@ -25,44 +26,46 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
* @author
*/
@Slf4j
@UtilityClass
public class AreaUtils {
/**
* SEARCHER
*/
@SuppressWarnings("InstantiationOfUtilityClass")
private final static AreaUtils INSTANCE = new AreaUtils();
/**
* Area 访
*/
private static Map<Integer, Area> areas;
private AreaUtils() {
long now = System.currentTimeMillis();
areas = new HashMap<>();
areas.put(Area.ID_GLOBAL, new Area(Area.ID_GLOBAL, "全球", 0,
null, new ArrayList<>()));
// 从 csv 中加载数据
List<CsvRow> rows = CsvUtil.getReader().read(ResourceUtil.getUtf8Reader("area.csv")).getRows();
rows.remove(0); // 删除 header
for (CsvRow row : rows) {
// 创建 Area 对象
Area area = new Area(Integer.valueOf(row.get(0)), row.get(1), Integer.valueOf(row.get(2)),
null, new ArrayList<>());
// 添加到 areas 中
areas.put(area.getId(), area);
}
static {
init();
}
// 构建父子关系:因为 Area 中没有 parentId 字段,所以需要重复读取
for (CsvRow row : rows) {
Area area = areas.get(Integer.valueOf(row.get(0))); // 自己
Area parent = areas.get(Integer.valueOf(row.get(3))); // 父
Assert.isTrue(area != parent, "{}:父子节点相同", area.getName());
area.setParent(parent);
parent.getChildren().add(area);
/**
*
*/
private static void init() {
try {
long now = System.currentTimeMillis();
areas = new HashMap<>();
areas.put(Area.ID_GLOBAL, new Area(Area.ID_GLOBAL, "全球", 0, null, new ArrayList<>()));
// 从 csv 中加载数据
List<CsvRow> rows = CsvUtil.getReader().read(ResourceUtil.getUtf8Reader("area.csv")).getRows();
rows.remove(0); // 删除 header
for (CsvRow row : rows) {
Area area = new Area(Integer.valueOf(row.get(0)), row.get(1), Integer.valueOf(row.get(2)), null, new ArrayList<>());
areas.put(area.getId(), area);
}
// 构建父子关系:因为 Area 中没有 parentId 字段,所以需要重复读取
for (CsvRow row : rows) {
Area area = areas.get(Integer.valueOf(row.get(0))); // 自己
Area parent = areas.get(Integer.valueOf(row.get(3))); // 父
Assert.isTrue(area != parent, "{}:父子节点相同", area.getName());
area.setParent(parent);
parent.getChildren().add(area);
}
log.info("启动加载 AreaUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now);
} catch (Exception e) {
throw new RuntimeException("AreaUtils 初始化失败", e);
}
log.info("启动加载 AreaUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now);
}
/**

View File

@ -3,11 +3,10 @@ package cn.iocoder.yudao.framework.ip.core.utils;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.iocoder.yudao.framework.ip.core.Area;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import org.lionsoul.ip2region.xdb.Searcher;
import java.io.IOException;
/**
* IP
*
@ -16,30 +15,29 @@ import java.io.IOException;
* @author wanglhup
*/
@Slf4j
@UtilityClass
public class IPUtils {
/**
* SEARCHER
*/
@SuppressWarnings("InstantiationOfUtilityClass")
private final static IPUtils INSTANCE = new IPUtils();
/**
* IP
*/
private static Searcher SEARCHER;
static {
init();
}
/**
*
*
*/
private IPUtils() {
private static void init() {
try {
long now = System.currentTimeMillis();
byte[] bytes = ResourceUtil.readBytes("ip2region.xdb");
SEARCHER = Searcher.newWithBuffer(bytes);
log.info("启动加载 IPUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now);
} catch (IOException e) {
log.error("启动加载 IPUtils 失败", e);
} catch (Exception e) {
throw new RuntimeException("IPUtils 初始化失败", e);
}
}

View File

@ -5,7 +5,12 @@ import cn.iocoder.yudao.framework.ip.core.Area;
import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* {@link AreaUtils}
@ -31,6 +36,46 @@ public class AreaUtilsTest {
assertEquals(AreaUtils.format(110105), "北京市 北京市 朝阳区");
assertEquals(AreaUtils.format(1), "中国");
assertEquals(AreaUtils.format(2), "蒙古");
// 中国台湾省:省/市/区三级
assertEquals(AreaUtils.format(710101), "台湾省 台北市 中正区");
// 自定义分隔符
assertEquals(AreaUtils.format(110105, "/"), "北京市/北京市/朝阳区");
// 不存在的编号
assertNull(AreaUtils.format(-1));
}
@Test
public void testParseArea() {
// 调用:通过路径解析得到地区
Area area = AreaUtils.parseArea("北京市/北京市/朝阳区");
// 断言
assertNotNull(area);
assertEquals(area.getId(), 110105);
// 路径不存在时返回 null
assertNull(AreaUtils.parseArea("不存在/路径"));
}
@Test
public void testGetParentIdByType() {
// 调用:朝阳区向上找省
Integer provinceId = AreaUtils.getParentIdByType(110105, AreaTypeEnum.PROVINCE);
// 断言
assertEquals(provinceId, 110000);
// 自身就是目标类型
assertEquals(AreaUtils.getParentIdByType(110000, AreaTypeEnum.PROVINCE), 110000);
// 不存在的编号返回 null
assertNull(AreaUtils.getParentIdByType(-1, AreaTypeEnum.PROVINCE));
}
@Test
public void testGetByType() {
// 调用:获取所有省份
List<Area> provinces = AreaUtils.getByType(AreaTypeEnum.PROVINCE, area -> area);
// 断言:包含北京、台湾、香港、澳门
assertTrue(provinces.stream().anyMatch(area -> "北京市".equals(area.getName())));
assertTrue(provinces.stream().anyMatch(area -> "台湾省".equals(area.getName())));
assertTrue(provinces.stream().anyMatch(area -> "香港特别行政区".equals(area.getName())));
assertTrue(provinces.stream().anyMatch(area -> "澳门特别行政区".equals(area.getName())));
}
}

View File

@ -42,8 +42,8 @@
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<!-- RPC 相关 -->

View File

@ -42,8 +42,8 @@
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有 ExcelUtils 使用 -->
</dependency>

View File

@ -41,8 +41,8 @@
<!-- 工具类相关 -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
</dependencies>

View File

@ -35,8 +35,8 @@
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有 TraceFilter 使用 -->
</dependency>

View File

@ -94,6 +94,13 @@
<groupId>com.fhs-opensource</groupId>
<artifactId>easy-trans-mybatis-plus-extend</artifactId>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -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));
}
/**
* 使 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
*
@ -145,6 +170,14 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
return CollUtil.getFirst(list);
}
/**
*
* <p>
* 使 selectOne
*/
default T selectLastOne(LambdaQueryWrapper<T> queryWrapper) {
return CollUtil.getLast(selectList(queryWrapper));
}
default Long selectCount() {
return selectCount(new QueryWrapper<>());

View File

@ -24,6 +24,12 @@ public class LambdaQueryWrapperX<T> extends LambdaQueryWrapper<T> {
}
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) {
if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {

View File

@ -27,6 +27,13 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
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) {
if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
return (MPJLambdaWrapperX<T>) super.in(column, values);
@ -102,7 +109,6 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
return this;
}
// ========== 重写父类方法,方便链式调用 ==========
@Override

View File

@ -25,6 +25,13 @@ public class QueryWrapperX<T> extends QueryWrapper<T> {
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) {
if (!CollectionUtils.isEmpty(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) {
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]);
}
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]);
}
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 this;

View File

@ -23,6 +23,7 @@ import net.sf.jsqlparser.schema.Table;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.regex.Pattern;
/**
* MyBatis
@ -31,6 +32,8 @@ public class MyBatisUtils {
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_]+)*$");
public static <T> Page<T> buildPage(PageParam pageParam) {
return buildPage(pageParam, null);
}
@ -42,8 +45,11 @@ public class MyBatisUtils {
// 排序字段
if (CollUtil.isNotEmpty(sortingFields)) {
for (SortingField sortingField : sortingFields) {
page.addOrder(new OrderItem().setAsc(SortingField.ORDER_ASC.equals(sortingField.getOrder()))
.setColumn(StrUtil.toUnderlineCase(sortingField.getField())));
String columnName = buildSafeOrderColumn(sortingField.getField());
if (columnName == null) {
continue;
}
page.addOrder(new OrderItem().setAsc(isAscOrder(sortingField.getOrder())).setColumn(columnName));
}
}
return page;
@ -57,23 +63,29 @@ public class MyBatisUtils {
if (wrapper instanceof QueryWrapper) {
QueryWrapper<T> query = (QueryWrapper<T>) wrapper;
for (SortingField sortingField : sortingFields) {
query.orderBy(true,
SortingField.ORDER_ASC.equals(sortingField.getOrder()),
StrUtil.toUnderlineCase(sortingField.getField()));
String columnName = buildSafeOrderColumn(sortingField.getField());
if (columnName == null) {
continue;
}
query.orderBy(true, isAscOrder(sortingField.getOrder()), columnName);
}
} else if (wrapper instanceof LambdaQueryWrapper) {
// LambdaQueryWrapper 不直接支持字符串字段排序,使用 last 方法拼接 ORDER BY
LambdaQueryWrapper<T> lambdaQuery = (LambdaQueryWrapper<T>) wrapper;
StringBuilder orderBy = new StringBuilder();
for (SortingField sortingField : sortingFields) {
String columnName = buildSafeOrderColumn(sortingField.getField());
if (columnName == null) {
continue;
}
if (StrUtil.isNotEmpty(orderBy)) {
orderBy.append(", ");
}
orderBy.append(StrUtil.toUnderlineCase(sortingField.getField()))
.append(" ")
.append(SortingField.ORDER_ASC.equals(sortingField.getOrder()) ? "ASC" : "DESC");
orderBy.append(columnName).append(" ").append(getOrderDirection(sortingField.getOrder()));
}
if (StrUtil.isNotEmpty(orderBy)) {
lambdaQuery.last("ORDER BY " + orderBy);
}
lambdaQuery.last("ORDER BY " + orderBy);
// 另外个思路https://blog.csdn.net/m0_59084856/article/details/138450913
} else {
throw new IllegalArgumentException("Unsupported wrapper type: " + wrapper.getClass().getName());
@ -81,6 +93,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

View File

@ -0,0 +1,106 @@
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.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.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));
}
private void assertOrderItem(OrderItem orderItem, String column, boolean asc) {
assertEquals(column, orderItem.getColumn());
assertEquals(asc, orderItem.isAsc());
}
}

View File

@ -36,11 +36,22 @@
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
<!--
TODO 芋艿WxJava 4.8.x 的 AbstractWxMpConfigStorageConfiguration 仍引用了 HttpClient 4.x 的
org.apache.http.ssl.TrustStrategy 类。升级 Spring Cloud Alibaba 到 2025.0.0.0 后Nacos 不再
传递 HttpClient 4.xhttpcore导致 ClassNotFoundException。
临时解决:显式引入 httpclient 4.x。待 WxJava 修复后移除。
-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
<!-- 工具相关 -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -9,6 +9,7 @@ import uk.co.jemos.podam.api.PodamFactory;
import uk.co.jemos.podam.api.PodamFactoryImpl;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Date;
@ -52,6 +53,10 @@ public class RandomUtils {
}
return RandomUtil.randomInt();
});
// BigDecimal限制精度在 DECIMAL(10,2) 范围内,避免 H2 等数据库溢出
PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(BigDecimal.class,
(dataProviderStrategy, attributeMetadata, map) ->
BigDecimal.valueOf(RandomUtil.randomInt(0, 10000000), 2));
// LocalDateTime
PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(LocalDateTime.class,
(dataProviderStrategy, attributeMetadata, map) -> randomLocalDateTime());

View File

@ -69,7 +69,7 @@ public class ApiAccessLogFilter extends ApiRequestFilter {
LocalDateTime beginTime = LocalDateTime.now();
// 提前获得参数,避免 XssFilter 过滤处理
Map<String, String> queryString = ServletUtils.getParamMap(request);
String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null;
String requestBody = ServletUtils.getBody(request);
try {
// 继续过滤器

View File

@ -44,7 +44,7 @@ public class ApiAccessLogInterceptor implements HandlerInterceptor {
// 打印 request 日志
if (!SpringUtils.isProd()) {
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)) {
log.info("[preHandle][开始请求 URL({}) 无参数]", request.getRequestURI());
} else {

View File

@ -37,8 +37,14 @@ public class BannerApplicationRunner implements ApplicationRunner {
System.out.println("[商城系统 yudao-module-mall 教程][参考 https://cloud.iocoder.cn/mall/build/ 开启]");
// ERP 系统
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 系统
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/ 开启]");
// 支付平台

View File

@ -410,25 +410,43 @@ public class GlobalExceptionHandler {
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"[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_")) {
log.error("[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"[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_")) {
log.error("[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]");
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]");
}
// 8. AI 大模型
// 11. AI 大模型
if (message.contains("ai_")) {
log.error("[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
}
// 9. IoT 物联网
// 12. IoT 物联网
if (message.contains("iot_")) {
log.error("[IoT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]");
return CommonResult.error(NOT_IMPLEMENTED.getCode(),

View File

@ -40,11 +40,6 @@
<artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId> <!-- 接口文档 -->
<artifactId>knife4j-gateway-spring-boot-starter</artifactId>

View File

@ -39,8 +39,14 @@ public class BannerApplicationRunner implements ApplicationRunner {
System.out.println("[商城系统 yudao-module-mall 教程][参考 https://cloud.iocoder.cn/mall/build/ 开启]");
// ERP 系统
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 系统
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/ 开启]");
// 支付平台

View File

@ -192,6 +192,27 @@ spring:
- Path=/admin-api/iot/**
filters:
- RewritePath=/admin-api/iot/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
## mes-server 服务
- id: mes-admin-api # 路由的编号
uri: grayLb://mes-server
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
- Path=/admin-api/mes/**
filters:
- 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:
prefix-enabled: false # 避免 Swagger 重复带上额外的 /admin-api/system 前缀
default-filters: # 全局过滤器,对应 GatewayFilterDefinition 数组
@ -251,9 +272,18 @@ knife4j:
- name: iot-server
service-name: iot-server
url: /admin-api/iot/v3/api-docs
- name: mes-server
service-name: mes-server
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
--- #################### 芋道相关配置 ####################
yudao:
info:
version: 1.0.0
version: 1.0.0

View File

@ -19,9 +19,9 @@
国外OpenAI、Ollama、Midjourney、StableDiffusion、Suno
</description>
<properties>
<spring-ai.version>1.1.2</spring-ai.version>
<spring-ai.version>1.1.5</spring-ai.version>
<!-- https://mvnrepository.com/artifact/com.alibaba.cloud.ai/spring-ai-alibaba -->
<alibaba-ai.version>1.1.0.0-RC2</alibaba-ai.version>
<alibaba-ai.version>1.1.2.2</alibaba-ai.version>
<tinyflow.version>1.2.6</tinyflow.version>
</properties>

View File

@ -83,6 +83,15 @@ public class AiKnowledgeSegmentController {
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除段落")
@Parameter(name = "id", description = "段落编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('ai:knowledge:delete')")
public CommonResult<Boolean> deleteKnowledgeSegment(@RequestParam("id") Long id) {
segmentService.deleteKnowledgeSegment(id);
return success(true);
}
@GetMapping("/split")
@Operation(summary = "切片内容")
@Parameters({

View File

@ -109,6 +109,7 @@ public class AiImageServiceImpl implements AiImageService {
}
@Async
@SuppressWarnings("ConstantValue")
public void executeDrawImage(AiImageDO image, AiImageDrawReqVO reqVO, AiModelDO model) {
try {
// 1.1 构建请求
@ -164,8 +165,8 @@ public class AiImageServiceImpl implements AiImageService {
.build();
} else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.TONG_YI.getPlatform())) {
return DashScopeImageOptions.builder()
.withModel(model.getModel()).withN(1)
.withHeight(draw.getHeight()).withWidth(draw.getWidth())
.model(model.getModel()).n(1)
.height(draw.getHeight()).width(draw.getWidth())
.build();
} else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.YI_YAN.getPlatform())) {
return QianFanImageOptions.builder()

View File

@ -98,6 +98,13 @@ public interface AiKnowledgeSegmentService {
*/
void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO);
/**
*
*
* @param id
*/
void deleteKnowledgeSegment(Long id);
/**
*
*

View File

@ -141,6 +141,19 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
}
}
@Override
public void deleteKnowledgeSegment(Long id) {
// 1. 校验段落存在
AiKnowledgeSegmentDO segment = validateKnowledgeSegmentExists(id);
// 2. 删除向量
VectorStore vectorStore = getVectorStoreById(segment.getKnowledgeId());
deleteVectorStore(vectorStore, segment);
// 3. 删除段落记录
segmentMapper.deleteById(id);
}
@Override
public void deleteKnowledgeSegmentByDocumentId(Long documentId) {
// 1. 查询需要删除的段落

View File

@ -1,7 +1,9 @@
package cn.iocoder.yudao.module.ai.util;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
@ -33,6 +35,28 @@ public class AiUtils {
public static final String TOOL_CONTEXT_LOGIN_USER = "LOGIN_USER";
public static final String TOOL_CONTEXT_TENANT_ID = "TENANT_ID";
/**
*
*
* @see <a href="https://bailian.console.aliyun.com/cn-beijing/?tab=model#/model-market/all?providers=qwen&capabilities=VU">广</a>
* @see <a href="https://help.aliyun.com/zh/model-studio/error-code#error-url"> withMultiModel </a>
*/
public static final Set<String> TONG_YI_MULTI_MODELS = SetUtils.asSet(
// qwen3.5 / 3.6 系列(统一多模态主干)
"qwen3.6-plus", "qwen3.6-flash",
"qwen3.5-plus", "qwen3.5-flash",
// qwen-vl 视觉理解
"qwen3-vl-plus", "qwen3-vl-flash",
"qwen-vl-max", "qwen-vl-plus",
"qwen2.5-vl-72b-instruct", "qwen2.5-vl-32b-instruct",
"qwen2.5-vl-7b-instruct", "qwen2.5-vl-3b-instruct",
// qvq 视觉推理
"qvq-max", "qvq-plus",
// qwen-omni 全模态
"qwen3.5-omni-plus", "qwen3.5-omni-flash",
"qwen3-omni-flash", "qwen-omni-turbo"
);
public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) {
return buildChatOptions(platform, model, temperature, maxTokens, null, null);
}
@ -44,9 +68,10 @@ public class AiUtils {
// noinspection EnhancedSwitchMigration
switch (platform) {
case TONG_YI:
return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens)
.withEnableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置
.withToolCallbacks(toolCallbacks).withToolContext(toolContext).build();
return DashScopeChatOptions.builder().model(model).temperature(temperature).maxToken(maxTokens)
.enableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置
.multiModel(TONG_YI_MULTI_MODELS.contains(model)) // 是否多模态模型
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case YI_YAN:
return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
case DEEP_SEEK:
@ -125,10 +150,13 @@ public class AiUtils {
|| response.getResult().getOutput() == null) {
return null;
}
if (response.getResult().getOutput() instanceof DeepSeekAssistantMessage) {
return ((DeepSeekAssistantMessage) (response.getResult().getOutput())).getReasoningContent();
AssistantMessage output = response.getResult().getOutput();
// DeepSeek 通过专属 AssistantMessage 暴露 reasoningContent
if (output instanceof DeepSeekAssistantMessage) {
return ((DeepSeekAssistantMessage) output).getReasoningContent();
}
return null;
// 通义千问等通过 metadata 透传 reasoningContent
return MapUtil.getStr(output.getMetadata(), "reasoningContent");
}
}

View File

@ -103,7 +103,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
--- #################### 监控相关配置 ####################

View File

@ -98,7 +98,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
--- #################### 监控相关配置 ####################

View File

@ -34,15 +34,15 @@ public class TongYiChatModelTests {
private final DashScopeChatModel chatModel = DashScopeChatModel.builder()
.dashScopeApi(DashScopeApi.builder()
.apiKey("sk-47aa124781be4bfb95244cc62f63f7d0")
.apiKey("sk-cd9f39e99ea54840bd1888282325f55a") // https://bailian.console.aliyun.com/cn-beijing/?tab=model#/api-key 获取密钥
.build())
.defaultOptions(DashScopeChatOptions.builder()
// .withModel("qwen1.5-72b-chat") // 模型
.withModel("qwen3-235b-a22b-thinking-2507") // 模型
// .withModel("deepseek-r1") // 模型deepseek-r1
// .withModel("deepseek-v3") // 模型deepseek-v3
// .withModel("deepseek-r1-distill-qwen-1.5b") // 模型deepseek-r1-distill-qwen-1.5b
// .withEnableThinking(true)
.multiModel(true) // 注意:当使用 qwen3.6-plus 等多模态模型,需要设置为 true可见 https://help.aliyun.com/zh/model-studio/error-code#error-url 链接
.model("qwen3.6-plus") // 模型
// .model("deepseek-r1") // 模型deepseek-r1
// .model("deepseek-v3") // 模型deepseek-v3
// .model("deepseek-r1-distill-qwen-1.5b") // 模型deepseek-r1-distill-qwen-1.5b
// .enableThinking(true)
.build())
.build();
@ -85,9 +85,9 @@ public class TongYiChatModelTests {
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
DashScopeChatOptions options = DashScopeChatOptions.builder()
.withModel("qwen3-235b-a22b-thinking-2507")
.model("qwen3.6-plus").multiModel(true)
// .withModel("qwen-max-2025-01-25")
.withEnableThinking(true) // 必须设置,否则会报错
.enableThinking(true) // 必须设置,否则会报错
.build();
// 调用
@ -112,8 +112,8 @@ public class TongYiChatModelTests {
Document document01 = new Document("abc");
Document document02 = new Document("sapring");
RerankOptions options = DashScopeRerankOptions.builder()
.withTopN(1)
.withModel("gte-rerank-v2")
.topN(1)
.model("gte-rerank-v2")
.build();
RerankRequest rerankRequest = new RerankRequest(
query,

View File

@ -12,23 +12,34 @@ import org.springframework.ai.image.ImageResponse;
/**
* {@link DashScopeImageModel}
*
* TODO @spring-ai-alibaba-dashscope1.1.2.2 {@code DashScopeImageApi#resolveImagePath} {@code wan2.7-image}
* {@code text2image/image-synthesis} + {@code prompt}
* {@code multimodal-generation/generation} + {@code messages}
* SDK
*
* SDK {@code wan2.6-image} {@code qwen-image}
* {@code DashScopeImageApi} {@code wan2.7*} {@code wan2.6-image} {@code image-generation/generation}
*
* @author fansili
*/
public class TongYiImagesModelTest {
private final DashScopeImageModel imageModel = DashScopeImageModel.builder()
.dashScopeApi(DashScopeImageApi.builder()
.apiKey("sk-47aa124781be4bfb95244cc62f63f7d0")
.apiKey("sk-cd9f39e99ea54840bd1888282325f55a") // https://bailian.console.aliyun.com/cn-beijing/?tab=model#/api-key 获取密钥
.build())
.build();
// TODO @芋艿:
@Test
@Disabled
public void imageCallTest() {
// 准备参数
ImageOptions options = DashScopeImageOptions.builder()
.withModel("wanx-v1")
.withHeight(256).withWidth(256)
.model("wan2.7-image")
// .withSize("2k")
.height(768).width(768)
.n(1)
.build();
ImagePrompt prompt = new ImagePrompt("中国长城!", options);

View File

@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* BPM
*
* @author Lesan
*/
@RequiredArgsConstructor
@Getter
public enum BpmConditionOpCodeEnum {
EQ("==", "等于", " var:getOrDefault(%s, null) == %s "),
NE("!=", "不等于", " var:getOrDefault(%s, null) != %s "),
GT(">", "大于", " var:getOrDefault(%s, null) > %s "),
GE(">=", "大于等于", " var:getOrDefault(%s, null) >= %s "),
LT("<", "小于", " var:getOrDefault(%s, null) < %s "),
LE("<=", "小于等于", " var:getOrDefault(%s, null) <= %s "),
CONTAINS("contain", "包含", " var:contains(%s, %s) "),
NOT_CONTAINS("!contain", "不包含", " !var:contains(%s, %s) ");
private final String code;
private final String des;
private final String symbol;
public static BpmConditionOpCodeEnum fromCode(String code) {
for (BpmConditionOpCodeEnum op : BpmConditionOpCodeEnum.values()) {
if (op.code.equalsIgnoreCase(code)) {
return op;
}
}
throw new IllegalArgumentException("未知操作符: " + code);
}
}

View File

@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form;
import lombok.Data;
import java.util.List;
/**
* VO
*/
@ -20,5 +22,9 @@ public class BpmFormFieldVO {
*
*/
private String title;
/**
*
*/
private List<BpmFormFieldVO> children;
}

View File

@ -66,15 +66,15 @@ public class BpmProcessInstanceCopyController {
Map<String, HistoricProcessInstance> processInstanceMap = processInstanceService.getHistoricProcessInstanceMap(
convertSet(pageResult.getList(), BpmProcessInstanceCopyDO::getProcessInstanceId));
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(pageResult.getList(),
copy -> Stream.of(copy.getStartUserId(), Long.parseLong(copy.getCreator()))));
copy -> Stream.of(copy.getStartUserId(), copy.getUserId())));
Map<String, BpmProcessDefinitionInfoDO> processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap(
convertSet(pageResult.getList(), BpmProcessInstanceCopyDO::getProcessDefinitionId));
return success(convertPage(pageResult, copy -> {
BpmProcessInstanceCopyRespVO copyVO = BeanUtils.toBean(copy, BpmProcessInstanceCopyRespVO.class);
MapUtils.findAndThen(userMap, Long.valueOf(copy.getCreator()),
user -> copyVO.setStartUser(BeanUtils.toBean(user, UserSimpleBaseVO.class)));
MapUtils.findAndThen(userMap, copy.getStartUserId(),
MapUtils.findAndThen(userMap, copy.getUserId(),
user -> copyVO.setCreateUser(BeanUtils.toBean(user, UserSimpleBaseVO.class)));
MapUtils.findAndThen(userMap, copy.getStartUserId(),
user -> copyVO.setStartUser(BeanUtils.toBean(user, UserSimpleBaseVO.class)));
MapUtils.findAndThen(processInstanceMap, copyVO.getProcessInstanceId(),
processInstance -> {
copyVO.setSummary(FlowableUtils.getSummary(

View File

@ -42,7 +42,7 @@ public class BpmOALeaveDO extends BaseDO {
/**
*
*/
private String type;
private Integer type;
/**
*
*/

View File

@ -14,6 +14,7 @@ import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior;
import org.flowable.engine.impl.bpmn.behavior.SequentialMultiInstanceBehavior;
import org.flowable.engine.impl.persistence.entity.ExecutionEntity;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
@ -53,7 +54,7 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
@SuppressWarnings("unchecked")
Set<Long> assigneeUserIds = (Set<Long>) execution.getVariableLocal(super.collectionVariable, Set.class);
if (assigneeUserIds == null) {
assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution);
assigneeUserIds = new LinkedHashSet<>(taskCandidateInvoker.calculateUsersByTask(execution));
if (CollUtil.isEmpty(assigneeUserIds)) {
// 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过!
// 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务

View File

@ -7,10 +7,10 @@ import org.springframework.stereotype.Component;
/**
* variable
*
* ConditionNodeConvert buildConditionExpression
*
* @deprecated
* @author jason
*/
@Deprecated // TODO @芋艿:兼容老版本,预计 27 年删除;
@Component
public class VariableConvertByTypeExpressionFunction extends AbstractFlowableVariableExpressionFunction {

View File

@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
@ -11,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.enums.definition.BpmModelFormTypeEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.SneakyThrows;
import org.flowable.common.engine.api.delegate.Expression;
import org.flowable.common.engine.api.variable.VariableContainer;
@ -26,6 +28,7 @@ import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.TaskInfo;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -244,12 +247,10 @@ public class FlowableUtils {
}
// 解析表单配置
Map<String, BpmFormFieldVO> formFieldsMap = new HashMap<>();
Map<String, BpmFormFieldVO> formFieldsMap = new LinkedHashMap<>();
processDefinitionInfo.getFormFields().forEach(formFieldStr -> {
BpmFormFieldVO formField = JsonUtils.parseObject(formFieldStr, BpmFormFieldVO.class);
if (formField != null) {
formFieldsMap.put(formField.getField(), formField);
}
JsonNode formFieldNode = JsonUtils.parseObject(formFieldStr, JsonNode.class);
parseFormField(formFieldNode, formFieldsMap);
});
// 情况一:当自定义了摘要
@ -273,6 +274,40 @@ public class FlowableUtils {
.collect(Collectors.toList());
}
/**
*
*/
private static void parseFormField(JsonNode formFieldNode, Map<String, BpmFormFieldVO> formFieldsMap) {
if (formFieldNode == null || !formFieldNode.isObject()) {
return;
}
// 如果 children 里存在对象节点,说明是布局组件;字符串节点是分割线、标签、文字等展示组件内容,直接跳过。
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);
}
if (hasObjectChild) {
return;
}
}
// 真实字段才加入 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())) {
formFieldsMap.put(formField.getField(), formField);
}
}
// ========== Task 相关的工具方法 ==========
/**

View File

@ -707,10 +707,9 @@ public class SimpleModelUtils {
List<String> list = convertList(item.getRules(), (rule) -> {
String rightSide = NumberUtil.isNumber(rule.getRightSide()) ? rule.getRightSide()
: "\"" + rule.getRightSide() + "\""; // 如果非数值类型加引号
return String.format(" vars:getOrDefault(%s, null) %s var:convertByType(%s,%s) ",
return String.format(BpmConditionOpCodeEnum.fromCode(rule.getOpCode()).getSymbol(),
rule.getLeftSide(), // 左侧:读取变量
rule.getOpCode(), // 中间:操作符,比较
rule.getLeftSide(), rightSide); // 右侧转换变量VariableConvertByTypeExpressionFunction
rightSide); // 右侧:取值变量
});
// 构造条件组的表达式
Boolean and = item.getAnd();

View File

@ -1,46 +0,0 @@
package cn.iocoder.yudao.module.bpm.service.definition.dto;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
import lombok.Data;
/**
* BPM MetaInfo Response DTO
* { Model#setMetaInfo(String)}
*
* {@link cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO}
*
* @author
*/
@Data
public class BpmModelMetaInfoRespDTO {
/**
*
*/
private String icon;
/**
*
*/
private String description;
/**
*
*/
private Integer formType;
/**
*
* {@link BpmModelFormTypeEnum#NORMAL}
*/
private Long formId;
/**
* 使 Vue
* {@link BpmModelFormTypeEnum#CUSTOM}
*/
private String formCustomCreatePath;
/**
* 使 Vue
* {@link BpmModelFormTypeEnum#CUSTOM}
*/
private String formCustomViewPath;
}

View File

@ -87,7 +87,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
--- #################### 监控相关配置 ####################

View File

@ -98,7 +98,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
--- #################### 监控相关配置 ####################

View File

@ -224,7 +224,7 @@ public class BpmTaskCandidateInvokerTest extends BaseMockitoUnitTest {
when(adminUserApi.getUserMap(eq(asSet(1L, 2L)))).thenReturn(userMap);
// mock 方法empty
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));
// 调用

View File

@ -13,7 +13,6 @@ import org.mockito.InjectMocks;
import org.mockito.Mock;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

View File

@ -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\"}";
}

View File

@ -27,10 +27,12 @@ public interface CrmCustomerLimitConfigMapper extends BaseMapperX<CrmCustomerLim
Integer type, Long userId, Long deptId) {
LambdaQueryWrapperX<CrmCustomerLimitConfigDO> query = new LambdaQueryWrapperX<CrmCustomerLimitConfigDO>()
.eq(CrmCustomerLimitConfigDO::getType, type);
query.apply("FIND_IN_SET({0}, user_ids) > 0", userId);
if (deptId != null) {
query.apply("FIND_IN_SET({0}, dept_ids) > 0", deptId);
}
query.and(w -> {
w.apply("FIND_IN_SET({0}, user_ids) > 0", userId);
if (deptId != null) {
w.or().apply("FIND_IN_SET({0}, dept_ids) > 0", deptId);
}
});
return selectList(query);
}

View File

@ -87,7 +87,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
--- #################### 监控相关配置 ####################

View File

@ -96,7 +96,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
--- #################### 监控相关配置 ####################

View File

@ -87,7 +87,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
--- #################### 监控相关配置 ####################

View File

@ -96,7 +96,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
--- #################### 监控相关配置 ####################

24
yudao-module-im/pom.xml Normal file
View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,5 @@
/**
* @author anhaohao
* @since 2024/3/9 8:59
*/
package cn.iocoder.yudao.module.im.api;

View File

@ -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

View File

@ -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-wordtrie 树高效匹配,支持忽略大小写 / 全半角 / 数字风格) -->
<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>

View File

@ -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/ 文章
}
}

View File

@ -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));
}
}

View File

@ -0,0 +1,58 @@
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.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 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.RestController;
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(List.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(), List.of()), ImFacePackUserRespVO.Item.class));
return vo;
});
return success(result);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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)));
}
}

View File

@ -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;
});
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

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