Compare commits

...

267 Commits

Author SHA1 Message Date
YunaiV d7cfe9c241 【同步】BOOT 和 CLOUD 的功能 2026-06-08 00:22:02 +08:00
YunaiV 0c69bedc34 【同步】BOOT 和 CLOUD 的功能 2026-06-08 00:14:03 +08:00
YunaiV f5a68ce6f3 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	sql/mysql/ruoyi-vue-pro.sql
#	yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java
#	yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/api/config/ConfigApiImpl.java
#	yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/s3/S3FileClientTest.java
#	yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/config/IotMessageBusProperties.java
2026-06-07 23:36:09 +08:00
YunaiV bccf7c79e5 【同步】BOOT 和 CLOUD 的功能 2026-06-07 23:35:29 +08:00
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
YunaiV a3db49babf (〃'▽'〃) v2026.01 发布:大大大大完善 vben5 的 antd、vben 版本的功能,新增 IoT 各种接入协议 2026-01-29 23:32:29 +08:00
YunaiV 33ff11edcf Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	pom.xml
#	yudao-dependencies/pom.xml
2026-01-29 23:31:28 +08:00
YunaiV b8a213849c (〃'▽'〃) v2026.01 发布:大大大大完善 vben5 的 antd、vben 版本的功能,新增 IoT 各种接入协议 2026-01-29 23:29:40 +08:00
YunaiV 5a0d95e493 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud 2026-01-29 22:39:15 +08:00
YunaiV 622db6dc73 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/config/BpmFlowableConfiguration.java
#	yudao-module-bpm/yudao-module-bpm-server/src/main/resources/application-local.yaml
2026-01-29 22:38:27 +08:00
YunaiV 456c96df16 【同步】BOOT 和 CLOUD 的功能 2026-01-29 22:38:05 +08:00
YunaiV 560636ed50 【同步】BOOT 和 CLOUD 的功能 2026-01-29 22:36:17 +08:00
YunaiV 8fd3173670 【同步】BOOT 和 CLOUD 的功能 2026-01-29 22:29:41 +08:00
YunaiV c57d3a65f9 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	yudao-dependencies/pom.xml
#	yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceAuthReqDTO.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqttws/IotMqttWsDownstreamSubscriber.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqttws/IotMqttWsUpstreamProtocol.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/controller/admin/device/vo/device/IotDeviceImportExcelVO.java
#	yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java
#	yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java
#	yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java
#	yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageService.java
#	yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImpl.java
#	yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotWebSocketDataRuleAction.java
#	yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/websocket/IotWebSocketClient.java
#	yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceImpl.java
#	yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/vo/AuthLoginReqVO.java
2026-01-29 22:25:16 +08:00
YunaiV 2a48bcbee9 Merge remote-tracking branch 'origin/master-jdk17' into master-jdk17
# Conflicts:
#	yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/config/BpmFlowableConfiguration.java
2026-01-29 22:14:16 +08:00
YunaiV fa72dc4e59 【同步】BOOT 和 CLOUD 的功能 2026-01-29 22:14:05 +08:00
芋道源码 ad68973d19
!237 删除AsyncListenableTaskExecutor配置
Merge pull request !237 from alexz10248080/master
2026-01-25 12:55:07 +00:00
芋道源码 eee662adaf
!238 删除AsyncListenableTaskExecutor配置
Merge pull request !238 from alexz10248080/master-jdk17
2026-01-25 12:55:01 +00:00
芋道源码 35ccc5eaa4
!240 AuthLoginReqVO同步AuthRegisterReqVO、TenantSaveReqVO、UserSaveReqVO的username验证
Merge pull request !240 from kangxinghua/master
2026-01-25 12:43:56 +00:00
芋道源码 44bfa54d32
!239 AuthLoginReqVO同步AuthRegisterReqVO、TenantSaveReqVO、UserSaveReqVO的username验证
Merge pull request !239 from kangxinghua/master-jdk17
2026-01-25 12:43:10 +00:00
KangXinghua 4bd1ac8424 AuthLoginReqVO同步AuthRegisterReqVO、TenantSaveReqVO、UserSaveReqVO的username验证 2026-01-21 22:27:45 +08:00
KangXinghua 13b37b4d2f AuthLoginReqVO同步AuthRegisterReqVO、TenantSaveReqVO、UserSaveReqVO的username验证 2026-01-21 22:21:21 +08:00
zhengpingzhong d1fa308961 删除AsyncListenableTaskExecutor配置,官方commit(38727db7926ea470ae81eee6c95123123a3035cc)已经“Replace the use of the deprecated AsyncListenableTaskExecutor with AsyncTaskExecutor”,且ProcessEngineAutoConfiguration会注入 2026-01-20 16:03:34 +08:00
zhengpingzhong 9ef140f07f 删除AsyncListenableTaskExecutor配置,官方commit(38727db7926ea470ae81eee6c95123123a3035cc)已经“Replace the use of the deprecated AsyncListenableTaskExecutor with AsyncTaskExecutor”,且ProcessEngineAutoConfiguration会注入 2026-01-19 17:35:30 +08:00
YunaiV 99ffe0fd41 【同步】BOOT 和 CLOUD 的功能 2026-01-18 19:19:42 +08:00
YunaiV e10be5160b 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/rule/scene/IotSceneRuleServiceImpl.java
#	yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImplTest.java
2026-01-18 19:04:00 +08:00
YunaiV 17a1af1069 【同步】BOOT 和 CLOUD 的功能 2026-01-18 19:03:30 +08:00
YunaiV 304b2f102a 【同步】BOOT 和 CLOUD 的功能 2026-01-18 19:01:29 +08:00
YunaiV 2e317b165b fix:【crm】CrmProductRespVO 的 easy-trans 翻译错误,对应 https://t.zsxq.com/o6w8b 2026-01-18 18:01:28 +08:00
YunaiV 61b6b6c7bd fix:【system】MailSendServiceImplTest 单测报错的问题,MockedStatic 存在差异 2026-01-18 17:56:44 +08:00
YunaiV bcf00e3332 【同步】BOOT 和 CLOUD 的功能 2026-01-18 17:29:49 +08:00
YunaiV c95268dfba Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud 2026-01-18 17:22:52 +08:00
YunaiV 09d58706cc 【同步】BOOT 和 CLOUD 的功能 2026-01-18 17:22:44 +08:00
YunaiV c6f8680da3 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImplTest.java
2026-01-18 17:14:18 +08:00
YunaiV f6eff05053 【同步】BOOT 和 CLOUD 的功能 2026-01-18 17:13:07 +08:00
芋道源码 efae658cc5
!229 采用 https://github.com/spring-cloud/spring-cloud-gateway/pull/866 解决响应头重复问题
Merge pull request !229 from alexz10248080/master
2026-01-18 08:58:50 +00:00
YunaiV d5ed2f4728 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	yudao-gateway/src/main/resources/application.yaml
#	yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/mail/dto/MailSendSingleToUserReqDTO.java
#	yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/api/mail/MailSendApiImpl.java
#	yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/mq/message/mail/MailSendMessage.java
#	yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/mq/producer/mail/MailProducer.java
#	yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImpl.java
2026-01-18 16:58:19 +08:00
芋道源码 9defcf736c
!228 采用spring-cloud-gateway pull/866 解决响应头重复问题
Merge pull request !228 from alexz10248080/master-jdk17
2026-01-18 08:57:09 +00:00
Danlin b292bc2c19 !233 【system】发送邮件时,携带附件
* 发送邮件时,带上附件
2026-01-18 07:28:25 +00:00
芋道源码 72c0c013a0
!231 feat(trade): 优化订单日志切面中的用户信息获取逻辑
Merge pull request !231 from wuKong/feat(trade)-优化订单日志切面中的用户信息获取逻辑
2026-01-18 07:26:38 +00:00
YunaiV 6b519a7654 https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1485/files 2026-01-09 19:16:16 +08:00
wuKong 29e4976da3 feat(trade): 优化订单日志切面中的用户信息获取逻辑
- 使用 ThreadLocal 获取用户类型和用户ID,提高性能
- 添加空值检查确保在 ThreadLocal 未设置时回退到原方法
- 将订单ID检查从 == null 改为 ObjectUtil.isNull 提高一致性
2026-01-05 10:15:13 +08:00
zhengpingzhong d61554c9bb 采用 https://github.com/spring-cloud/spring-cloud-gateway/pull/866 解决响应头重复问题 2026-01-04 10:28:31 +08:00
YunaiV 6e9933cf0f fix:【system】getUser 默认忽略数据权限,基础数据较为通用 2026-01-02 17:15:24 +08:00
zhengpingzhong edb74f64d9 采用 https://github.com/spring-cloud/spring-cloud-gateway/pull/866 解决响应头重复问题 2025-12-30 14:07:06 +08:00
YunaiV 51da2a2f36 (〃'▽'〃) v2025.12 发布:极大极大完善 vben5 的 antd、vben 版本的功能,新增 admin uniapp vue3 版本 2025-12-28 10:52:44 +08:00
YunaiV fead40e564 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	pom.xml
#	yudao-dependencies/pom.xml
2025-12-28 10:51:23 +08:00
YunaiV 716e2a63c1 (〃'▽'〃) v2025.12 发布:极大极大完善 vben5 的 antd、vben 版本的功能,新增 admin uniapp vue3 版本 2025-12-28 10:50:53 +08:00
YunaiV 7739c2a35d Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	yudao-module-mall/yudao-module-promotion-server/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateService.java
#	yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImpl.java
2025-12-28 10:22:38 +08:00
YunaiV 0f27c0aa72 【同步】BOOT 和 CLOUD 的功能 2025-12-28 10:22:05 +08:00
YunaiV cad6da432b Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	yudao-dependencies/pom.xml
2025-12-27 21:43:00 +08:00
YunaiV f922d3fd55 fix:【mall】优惠劵:相关的判断不对,关联 https://t.zsxq.com/fYaKv 2025-12-27 21:41:46 +08:00
YunaiV 3b4d1ef4d8 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud 2025-12-27 21:41:10 +08:00
芋道源码 291d705307
!226 修改知识库和联网搜索仅初始化一次
Merge pull request !226 from 瞿佐鹏/feat/ai-stream-resp-fit
2025-12-27 03:07:31 +00:00
YunaiV 0869ad0513 feat:【infra】spring boot admin 默认需要登录 2025-12-27 10:30:47 +08:00
YunaiV e60f5496fa Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud 2025-12-27 10:29:58 +08:00
YunaiV be65f9cf97 feat:【infra】spring boot admin 默认需要登录 2025-12-27 10:29:42 +08:00
YunaiV bc4d8f85ad Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud 2025-12-25 22:52:32 +08:00
YunaiV c93f2ac2a4 【升级】dynamic-datasource from 4.3.1 to 4.5.0 2025-12-25 22:52:20 +08:00
YunaiV 15a2f2611f Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud 2025-12-25 22:48:22 +08:00
YunaiV 845315ff8a 【升级】jimubi from 2.2.0 to 2.3.0 2025-12-25 22:47:39 +08:00
YunaiV acbbdf2237 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	pom.xml
#	yudao-dependencies/pom.xml
2025-12-25 22:45:29 +08:00
YunaiV 742a6f3caf 【升级】spring-boot from 3.5.8 to 3.5.9 2025-12-25 22:45:13 +08:00
YunaiV 45b06e285a 【同步】BOOT 和 CLOUD 的功能 2025-12-25 22:34:17 +08:00
YunaiV 382e9e8cdd Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	yudao-dependencies/pom.xml
#	yudao-module-crm/yudao-module-crm-server/src/main/java/cn/iocoder/yudao/module/crm/service/contract/listener/CrmContractStatusListener.java
#	yudao-module-crm/yudao-module-crm-server/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/listener/CrmReceivableStatusListener.java
2025-12-25 22:30:00 +08:00
YunaiV 8e07be59d5 Merge remote-tracking branch 'origin/master-jdk17' into master-jdk17 2025-12-25 22:28:19 +08:00
YunaiV 201711cec1 【升级】mybatis-plus from 3.5.14 to 3.5.15
【升级】mybatis-plus-join from 1.5.4 to 1.5.5
【升级】rocketmq-spring from 2.3.4 to 2.3.5
【升级】netty from 4.2.7 to 4.2.9
【升级】awssdk from 2.39.2 to 2.40.15
2025-12-25 22:28:13 +08:00
瞿佐鹏 677110ff91 修改知识库和联网搜索仅初始化一次 2025-12-23 16:43:12 +08:00
芋道源码 bec1b083ec
!224 fix(system): 当用户没有绑定微信小程序时获取到socialUser为null,会导致发货失败
Merge pull request !224 from wuKong/fix(system)-当用户没有绑定微信小程序时获取到socialUser为null,会导致发货失败
2025-12-20 09:45:40 +00:00
YunaiV 7015d221b1 fix:【bpm】CrmContractStatusListener、CrmReceivableStatusListener 无需 @FeignClient(name = ApiConstants.NAME) 注解,对应 https://t.zsxq.com/8ETgM 2025-12-20 15:08:09 +08:00
YunaiV e63b792741 chore:升级 spring-ai 至 1.1.2,alibaba-ai 至 1.1.0.0-RC2 2025-12-20 10:21:33 +08:00
wuKong e455572adb refactor(system): 替换 ObjectUtil 为 ObjUtil
- 将 SocialClientApiImpl 中的 ObjectUtil.isNull 替换为 ObjUtil.isNull
- 移除未使用的 ObjectUtil 导入包
- 保持空值判断逻辑一致性
2025-12-15 18:58:13 +08:00
wuKong 13a1993dd2 fix(system): 当用户没有绑定微信小程序时获取到socialUser为null,会导致发货失败
- 在发送订阅消息前增加社交用户空值判断
- 避免因社交用户信息缺失导致的空指针异常
- 完善日志记录,提高问题排查效率
2025-12-15 18:55:08 +08:00
YunaiV 4017f71d10 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud 2025-12-13 12:33:06 +08:00
YunaiV ec8577bdd9 【同步】BOOT 和 CLOUD 的功能 2025-12-13 12:32:46 +08:00
YunaiV 84834c7a65 【同步】BOOT 和 CLOUD 的功能 2025-12-07 18:23:49 +08:00
YunaiV 667cf9d1c9 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud 2025-12-07 18:20:52 +08:00
YunaiV b5a7350fe9 【同步】BOOT 和 CLOUD 的功能 2025-12-07 18:20:05 +08:00
YunaiV af5bb360bf fix:一键改包后,RestTemplate 的冲突 2025-12-07 18:11:18 +08:00
YunaiV f4ba70ec5a feat:【SocialClient】 支付宝小程序登录补充 2025-11-29 10:10:31 +08:00
YunaiV c3bfdcf99b Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud 2025-11-26 23:09:01 +08:00
YunaiV 16e66ba175 (〃'▽'〃) v2025.11 发布:极大完善 vben5 的 antd、vben 版本的功能,优化整体稳定性 2025-11-26 23:07:13 +08:00
YunaiV 4cfff2d05c (〃'▽'〃) v2025.11 发布:极大完善 vben5 的 antd、vben 版本的功能,优化整体稳定性 2025-11-26 22:58:52 +08:00
YunaiV d52e6cb7ea Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	pom.xml
#	yudao-dependencies/pom.xml
2025-11-26 22:57:01 +08:00
YunaiV 63d1e9403b (〃'▽'〃) v2025.11 发布:极大完善 vben5 的 antd、vben 版本的功能,优化整体稳定性 2025-11-26 22:53:42 +08:00
YunaiV 454e68c162 fix:【system】AdminUserServiceImplTest 单测报错问题 2025-11-26 22:49:06 +08:00
YunaiV 12126a7530 【同步】BOOT 和 CLOUD 的功能 2025-11-26 22:45:16 +08:00
YunaiV 48a7348ff1 feat:【mp】完善“模版消息”的功能 2025-11-26 18:48:19 +08:00
YunaiV b8aa125955 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud 2025-11-26 18:47:42 +08:00
芋道源码 cc2aa364d5
!222 feat:【mp】完善“模版消息”的功能
Merge pull request !222 from 芋道源码/develop
2025-11-26 10:46:01 +00:00
YunaiV 036bdc79ba feat:【mp】完善“模版消息”的功能(调整包结构) 2025-11-26 18:43:50 +08:00
YunaiV a552b5a814 feat:【mp】完善“模版消息”的功能 2025-11-26 18:36:26 +08:00
YunaiV f2f0d143b5 Merge branch 'develop' of https://gitee.com/zhijiantianya/yudao-cloud 2025-11-26 18:35:33 +08:00
YunaiV bd31e5abb2 feat:【mp】完善“模版消息”的功能 2025-11-26 18:35:24 +08:00
YunaiV d369c8210c Merge branch 'develop' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	yudao-module-mp/yudao-module-mp-server/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/vo/MpUserPageReqVO.java
2025-11-26 18:34:57 +08:00
YunaiV a1f7f81e8d feat:【mp】完善“模版消息”的功能 2025-11-26 18:34:03 +08:00
YunaiV 04fb7b107e feat:【mp】完善“模版消息”的功能 2025-11-26 18:30:07 +08:00
芋道源码 b6f75b3825
!215 feat(mp): 新增公众号消息模板功能
Merge pull request !215 from wuKong/feat(mp)-新增公众号消息模板
2025-11-26 02:49:05 +00:00
YunaiV 78f4c7f3f7 chore:spring ai alibaba from 1.0.1 to 1.1.0 2025-11-26 09:55:42 +08:00
YunaiV 6dcbb1829b 【同步】BOOT 和 CLOUD 的功能 2025-11-25 13:00:42 +08:00
YunaiV 8becb49a3b 【同步】BOOT 和 CLOUD 的功能 2025-11-25 12:58:25 +08:00
YunaiV 739487e528 !221 feat(pay): 支持通过支付订单号查询支付订单 2025-11-24 20:20:03 +08:00
芋道源码 21f90806f4
!221 feat(pay): 支持通过支付订单号查询支付订单
Merge pull request !221 from wuKong/feat(pay)-支持通过订单号查询支付订单
2025-11-24 12:09:59 +00:00
YunaiV 6c295f8782 fix:【iot】spring redis 配置多了 data 层,修复 https://t.zsxq.com/51v5n 2025-11-24 19:25:28 +08:00
YunaiV a483d57eca feat:【iot】tdengine 调整成 WS 以支持 decimal 字段 2025-11-24 19:21:58 +08:00
wuKong 76380b5e5d 🎨 fix(pay):处理格式 2025-11-24 18:36:55 +08:00
YunaiV a1c07f1924 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud 2025-11-24 18:24:19 +08:00
YunaiV 5dc81ab895 fix:【iot】单测报错的问题 2025-11-24 18:24:10 +08:00
YunaiV 9858d5e42a fix:iot-gateway-server 的 spring-boot-maven-plugin 缺少版本号,对应 https://gitee.com/zhijiantianya/yudao-cloud/issues/ID5LU4 2025-11-24 18:04:28 +08:00
YunaiV 5a4a3a2e5d Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	yudao-dependencies/pom.xml
2025-11-24 17:45:23 +08:00
YunaiV c20433df33 chore:依赖升级最新版本 2025-11-24 17:43:58 +08:00
wuKong 4212c8aed8 fix(pay):修复支付订单同步后查询参数错误
- 修复了同步支付订单后查询参数传错的问题
- 将错误的 id 参数替换为正确的 order.getId() 参数
- 确保同步后能正确获取最新的订单状态
2025-11-24 17:37:22 +08:00
YunaiV c234029d9d Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	yudao-dependencies/pom.xml
2025-11-24 17:18:09 +08:00
YunaiV 6d5bb3f1bd chore:依赖升级最新版本 2025-11-24 17:15:17 +08:00
wuKong 8bfc3b480b feat(pay): 支持通过订单号查询支付订单
- 在 AppPayOrderController 中增加根据支付订单号查询订单的功能
- 新增 PayOrderMapper 的 selectByNo 方法用于按订单号查询
- 扩展 PayOrderService 接口及实现类以支持订单号查询
- 优化查询逻辑,优先使用订单号查询,其次使用订单ID查询
- 更新接口文档,增加支付订单号参数说明
- 使用 CharSequenceUtil 工具类判断订单号是否为空
2025-11-24 17:05:48 +08:00
YunaiV 45158ab898 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	pom.xml
#	yudao-dependencies/pom.xml
2025-11-24 16:59:44 +08:00
YunaiV e286630fdd chore:依赖升级最新版本 2025-11-24 16:56:55 +08:00
YunaiV 43fd4ef18f fix:【ai】单测在 spring-ai 1.1.0 版本下的报错 2025-11-24 16:56:04 +08:00
YunaiV 336b87dd3d Merge branch 'master-jdk17' of https://github.com/YunaiV/yudao-cloud into master-jdk17 2025-11-24 16:44:56 +08:00
YunaiV 1e1d560a25 fix: 更新 AI 平台名称从 '月之暗灭' 修改为 '月之暗面'(= = dota 打多了) 2025-11-24 15:18:43 +08:00
YunaiV 0b06e29877 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/IdTypeEnvironmentPostProcessor.java
2025-11-24 11:28:28 +08:00
YunaiV 14cfabfb9a 【同步】BOOT 和 CLOUD 的功能 2025-11-24 11:28:20 +08:00
YunaiV 4f1de29748 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts:
#	yudao-dependencies/pom.xml
#	yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/api/event/CrmContractStatusListener.java
#	yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/api/event/CrmReceivableStatusListener.java
2025-11-24 11:26:03 +08:00
YunaiV 3c1748aff1 !208 使用外部中间件时,如:金蝶、东方通 在不重启整个中间件的情况下,二次部署或多个服务同时部署在一个虚拟机下(JVM) IdTypeEnvir… 2025-11-24 11:24:52 +08:00
芋道源码 949ef86bbf
!208 使用外部中间件时,如:金蝶、东方通 在不重启整个中间件的情况下,二次部署或多个服务同时部署在一个虚拟机下(JVM) IdTypeEnvir…
Merge pull request !208 from 墨轩/master
2025-11-24 03:22:58 +00:00
YunaiV 187148ee8b fix: 【framework】TimestampLocalDateTimeSerializer 中,使用 ReflectUtil 替代 2025-11-24 11:18:53 +08:00
YunaiV d0d563ae61 【同步】BOOT 和 CLOUD 的功能(AI) 2025-11-24 11:11:40 +08:00
YunaiV f881cb74c5 【同步】BOOT 和 CLOUD 的功能(AI) 2025-11-24 11:10:53 +08:00
YunaiV e98716e526 【同步】BOOT 和 CLOUD 的功能 2025-11-24 10:54:10 +08:00
YunaiV a901928986 chore:flowable from 7.0.1 to 7.2.0 2025-11-23 17:36:36 +08:00
YunaiV 69e595d62e 【同步】BOOT 和 CLOUD 的功能 2025-11-23 09:07:03 +08:00
YunaiV a0b7777783 fix:【bpm】LoadBalanced 的 RestTemplate 场景下,影响普通域名的访问,解决 https://gitee.com/zhijiantianya/yudao-cloud/issues/ICY7H3 问题 2025-11-22 16:12:56 +08:00
芋道源码 a5aa2c84e3
!220 回退 'Pull Request !216 : feat(pay,mall-trade): 小程序商品订单详情path配置支持'
Merge pull request !220 from 芋道源码/revert-merge-216-master-jdk17
2025-11-21 13:21:49 +00:00
芋道源码 f63b837300
回退 'Pull Request !216 : feat(pay,mall-trade): 小程序商品订单详情path配置支持' 2025-11-21 13:21:34 +00:00
芋道源码 32260511d9
!216 feat(pay,mall-trade): 小程序商品订单详情path配置支持
Merge pull request !216 from wuKong/feat(pay,mall-trade)-小程序商品订单详情path配置
2025-11-21 13:19:22 +00:00
芋道源码 bb2f5827e9
Merge pull request #273 from cetuer/master-jdk17
fix:【bpm工作流】BpmProcessInstanceCopyServiceImpl抄送时参数顺序错误的问题
2025-11-21 20:33:33 +08:00
wuKong 45603c7132 fix(trade):优化订单详情接口参数处理
- 将同步支付状态参数改为字符串类型以避免空指针异常
- 添加参数非空判断逻辑
- 保留原有 Long 类型订单 ID 查询方式
- 增强接口对不同输入参数的兼容性处理
- 使用 Hutool 工具类进行参数转换与校验
- 维持原接口功能不变的基础上提升健壮性
2025-11-10 17:30:56 +08:00
wuKong d952cdec59 docs(order): 更新订单详情接口参数说明 2025-11-10 16:34:33 +08:00
wuKong 2d7bad4e98 🎨 feat(trade): 调整订单详情接口支持多种查询方式
- 修改 getOrderDetail 接口参数 id 类型为 String
- 根据 sync 参数决定调用不同的订单查询方法
- 删除冗余的 getOrderDetailByOutTradeNo 接口
- 优化订单支付状态同步逻辑
- 保持原有订单详情返回结构不变
2025-11-10 16:25:39 +08:00
wuKong 534b7e1f73 feat(pay,mall-trade): 小程序商品订单详情path配置支持
- 在 PayOrderApi 中新增 getMerchantOrderIdByPayOrderNo 方法
- 在 PayOrderApiImpl 中实现该方法,调用 service 层逻辑
- 在 PayOrderMapper 中增加 selectByNo 查询方法
- 在 PayOrderService 中定义 getMerchantOrderIdByNo 接口
- 在 PayOrderServiceImpl 中实现 getMerchantOrderIdByNo 业务逻辑
- 支持通过支付订单编号查询对应的商户订单编号feat(trade): 新增通过支付订单编号查询交易订单详情功能
- 在 AppTradeOrderController 中新增 getOrderDetailByOutTradeNo 接口
- 支持微信小程序订单中心跳转到订单详情页面
- 在 TradeOrderQueryService 中定义 getOrderByOutTradeNo 方法
- 在 TradeOrderQueryServiceImpl 中实现该方法
- 通过 PayOrderApi 调用获取商户订单编号
- 根据商户订单编号查询对应的交易订单信息
2025-11-10 11:36:47 +08:00
wuKong 0ec835244d feat(mp): 新增公众号消息模板功能
- 新增消息模板和模板发送记录的增删改查接口
- 新增消息模板和发送记录的分页查询及导出功能
- 新增批量发送模板消息功能
- 新增同步公众号模板功能
- 添加相关数据对象和错误码定义
- 扩展用户分页请求参数支持openid列表查询
- 完善模板消息相关的VO类和转换逻辑
2025-11-05 11:53:06 +08:00
zhangqb 9b891ecf3c fix:【bpm工作流】BpmProcessInstanceCopyServiceImpl抄送时参数顺序错误的问题 2025-10-28 14:18:45 +08:00
芋道源码 d64f4e9343
!211 修改iot中xxljob任务名称重复导致服务启动报错
Merge pull request !211 from huppygo/master-jdk17
2025-10-19 10:36:05 +00:00
huppygo 1618213494
修改iot中xxljob任务名称重复导致服务启动报错
Signed-off-by: huppygo <huppygo@qq.com>
2025-10-11 01:44:54 +00:00
墨轩 17f8047da0
IdTypeEnvironmentPostProcessor.java.
代码优化

Signed-off-by: 墨轩 <8813596+mx990420@user.noreply.gitee.com>
2025-09-24 02:43:31 +00:00
温艺伟 f12162e7ff 使用外部中间件时,如:金蝶、东方通 在不重启整个中间件的情况下,二次部署或多个服务同时部署在一个虚拟机下(JVM) IdTypeEnvironmentPostProcessor.setIdType 会将一个IdType对象put进SystemPropertiesPropertySource,而SystemPropertiesPropertySource在整个JVM中是共用的,导致两处问题:
报错信息:org.springframework.core.convert.ConverterNotFoundException:
        No converter found capable of converting from type [com.baomidou.mybatisplus.annotation.IdType] to type [com.baomidou.mybatisplus.annotation.IdType]
    问题一:IdTypeEnvironmentPostProcessor.getIdType 中 environment.getProperty(ID_TYPE_KEY, IdType.class) 获取到了上一次部署应用时的 IdType 对象,而上一次的 IdType 对象,和本次部署时 IdType.class 的类加载器不一致,导致报错;
    问题二:org.springframework.boot.context.properties.bind.BindConverter.convert 中,delegate.canConvert 返回的都是false,最终:throw (failure != null) ? failure : new ConverterNotFoundException(sourceType, targetType);
    原因分析:
    首先 ConfigurableEnvironment
        ConfigurableEnvironment.getProperty(...) 的查找顺序是分层次的:
        1、命令行参数(CommandLinePropertySource,即 --key=value)
        2、Java 系统属性(System.getProperties(),对应 SystemPropertiesPropertySource)
        3、操作系统环境变量(System.getenv(),对应 SystemEnvironmentPropertySource)
        4、application.yml / application.properties(OriginTrackedMapPropertySource)
        5、默认属性(DefaultPropertiesPropertySource)
    其次:Spring 的属性绑定用到了 ConfigurationPropertySource
        ConfigurationPropertySource:
            它是 Spring Boot 2.x 以后引入的抽象,表示配置属性的来源。
            比如:
            .properties / .yml 文件,
            系统属性(System.getProperties()),
            环境变量(System.getenv()),
            甚至 Nacos、Apollo 这样的远程配置中心。
            它统一成 ConfigurationPropertySource 接口,Spring Boot 就能用同一套逻辑去读取配置。
        和 ConfigurableEnvironment 的关系
            ConfigurableEnvironment 内部持有一系列 PropertySource。
            Spring Boot 启动时会把这些 PropertySource 适配成 ConfigurationPropertySource,
            这样属性绑定器(Binder)就可以从中读取配置值。
            也就是说:
            environment.getProperty("my.key") 读出来的值,
            和 Binder 里 ConfigurationPropertySource 提供的值,
            本质上是同一批配置源,只是走的 API 不一样。
        导致的问题:org.springframework.boot.context.properties.bind.BindConverter.convert 的参数:Object source, TypeDescriptor sourceType, TypeDescriptor targetType
            source 是上一次部署时的 IdType 对象
            sourceType 的类加载器 (sourceType.getType().getClassLoader()) 与 targetType 的类加载器 (targetType.getType().getClassLoader())不一致,抛出:ConverterNotFoundException

        org.springframework.boot.context.properties.bind.BindConverter.convert:
        private Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
            ConversionException failure = null;
            for (ConversionService delegate : this.delegates) {
                try {
                    if (delegate.canConvert(sourceType, targetType)) {
                        return delegate.convert(source, sourceType, targetType);
                    }
                }
                catch (ConversionException ex) {
                    if (failure == null && ex instanceof ConversionFailedException) {
                        failure = ex;
                    }
                }
            }
            throw (failure != null) ? failure : new ConverterNotFoundException(sourceType, targetType);
        }
2025-09-01 11:45:37 +08:00
3289 changed files with 252824 additions and 19772 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 修改,**重构优化**后端的代码,**美化**前端的界面。
>
@ -235,18 +235,19 @@
### 微信公众号
| | 功能 | 描述 |
|-----|--------|-------------------------------|
| 🚀 | 账号管理 | 配置接入的微信公众号,可支持多个公众号 |
| 🚀 | 数据统计 | 统计公众号的用户增减、累计用户、消息概况、接口分析等数据 |
| 🚀 | 粉丝管理 | 查看已关注、取关的粉丝列表,可对粉丝进行同步、打标签等操作 |
| 🚀 | 消息管理 | 查看粉丝发送的消息列表,可主动回复粉丝消息 |
| 🚀 | 自动回复 | 自动回复粉丝发送的消息,支持关注回复、消息回复、关键字回复 |
| 🚀 | 标签管理 | 对公众号的标签进行创建、查询、修改、删除等操作 |
| 🚀 | 菜单管理 | 自定义公众号的菜单,也可以从公众号同步菜单 |
| 🚀 | 素材管理 | 管理公众号的图片、语音、视频等素材,支持在线播放语音、视频 |
| 🚀 | 图文草稿箱 | 新增常用的图文素材到草稿箱,可发布到公众号 |
| 🚀 | 图文发表记录 | 查看已发布成功的图文素材,支持删除操作 |
| | 功能 | 描述 |
|----|--------|-------------------------------|
| 🚀 | 账号管理 | 配置接入的微信公众号,可支持多个公众号 |
| 🚀 | 数据统计 | 统计公众号的用户增减、累计用户、消息概况、接口分析等数据 |
| 🚀 | 粉丝管理 | 查看已关注、取关的粉丝列表,可对粉丝进行同步、打标签等操作 |
| 🚀 | 消息管理 | 查看粉丝发送的消息列表,可主动回复粉丝消息 |
| 🚀 | 模版消息 | 配置和发送模版消息,用于向粉丝推送通知类消息 |
| 🚀 | 自动回复 | 自动回复粉丝发送的消息,支持关注回复、消息回复、关键字回复 |
| 🚀 | 标签管理 | 对公众号的标签进行创建、查询、修改、删除等操作 |
| 🚀 | 菜单管理 | 自定义公众号的菜单,也可以从公众号同步菜单 |
| 🚀 | 素材管理 | 管理公众号的图片、语音、视频等素材,支持在线播放语音、视频 |
| 🚀 | 图文草稿箱 | 新增常用的图文素材到草稿箱,可发布到公众号 |
| 🚀 | 图文发表记录 | 查看已发布成功的图文素材,支持删除操作 |
### 商城系统
@ -272,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/>
@ -286,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) |
## 🐨 技术栈
### 微服务
@ -303,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>2025.10-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.38</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

@ -52,6 +52,7 @@ def load_and_clean(sql_file: str) -> str:
REPLACE_PAIR_LIST = (
(")\nVALUES ", ") VALUES "),
(" CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ", " "),
(" CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci ", " "),
(" KEY `", " INDEX `"),
("UNIQUE INDEX", "UNIQUE KEY"),
("b'0'", "'0'"),
@ -61,17 +62,33 @@ 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
content = re.sub(r'`([^`]+)`\(\d+\)', r'`\1`', content)
content = re.sub(r'\s+USING\s+BTREE\s+COMMENT\s+\'[^\']+\'', '', content)
content = re.sub(r"ENGINE.*COMMENT", "COMMENT", content)
content = re.sub(r"ENGINE.*;", ";", content)
return content
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:
@ -165,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}`"
@ -176,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:
@ -189,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"]:
@ -208,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
@ -221,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):
@ -245,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:
@ -260,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(
"unicode_escape"
)[1:-1]
table_ddl["comment"] = bytes(table_ddl["comment"], "utf-8").decode(
"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)
@ -314,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:
@ -345,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"]]
@ -372,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"
)
@ -401,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
@ -446,6 +550,8 @@ INSERT INTO dual VALUES (1);
class OracleConvertor(Convertor):
reserved_column_names = {"level", "size"}
def __init__(self, src):
super().__init__(src, "Oracle")
@ -490,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"]]
@ -518,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"
)
@ -550,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}')",
@ -871,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"
@ -889,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"]]
@ -909,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,32 +14,32 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>2025.10-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.14</mybatis-plus.version>
<mybatis-plus-join.version>1.5.4</mybatis-plus-join.version>
<dynamic-datasource.version>4.3.1</dynamic-datasource.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.51.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.3</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.4</rocketmq-spring.version>
<rocketmq-spring.version>2.3.5</rocketmq-spring.version>
<!-- RPC 相关 -->
<!-- Config 配置中心相关 -->
<!-- Job 定时任务相关 -->
@ -52,39 +52,47 @@
<opentracing.version>0.33.0</opentracing.version>
<!-- Test 测试相关 -->
<podam.version>7.2.11.RELEASE</podam.version> <!-- Spring Boot 2.X 最多使用 7.2.11 版本 -->
<jedis-mock.version>1.1.11</jedis-mock.version>
<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.38</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.40</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.4.8-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.11.1</commons-net.version>
<commons-lang3.version>3.18.0</commons-lang3.version>
<jsch.version>2.27.3</jsch.version>
<commons-net.version>3.13.0</commons-net.version>
<commons-lang3.version>3.20.0</commons-lang3.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.1.116.Final</netty.version>
<netty.version>4.2.14.Final</netty.version>
<mqtt.version>1.2.5</mqtt.version>
<pf4j-spring.version>0.9.0</pf4j-spring.version>
<vertx.version>4.5.13</vertx.version>
<vertx.version>4.5.26</vertx.version>
<okhttp.version>4.12.0</okhttp.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.30.14</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.1</jimureport.version>
<jimubi.version>2.1.0</jimubi.version>
<weixin-java.version>4.7.7-20250808.182223</weixin-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>
@ -305,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>
@ -368,7 +376,6 @@
<artifactId>yudao-spring-boot-starter-mq</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
@ -620,81 +627,24 @@
<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>
<version>${reflections.version}</version>
</dependency>
<!-- 三方云服务相关 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>${awssdk.version}</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
<version>${weixin-java.version}</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
<version>${weixin-java.version}</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
<version>${weixin-java.version}</version>
</dependency>
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
<version>${justauth.version}</version>
</dependency>
<dependency>
<groupId>com.xkcoding.justauth</groupId>
<artifactId>justauth-spring-boot-starter</artifactId>
<version>${justauth-starter.version}</version>
</dependency>
<!-- 积木报表-->
<dependency>
<groupId>org.jeecgframework.jimureport</groupId>
<artifactId>jimureport-spring-boot-starter</artifactId>
<version>${jimureport.version}</version>
</dependency>
<dependency>
<groupId>org.jeecgframework.jimureport</groupId>
<artifactId>jimubi-spring-boot-starter</artifactId>
<version>${jimubi.version}</version>
<exclusions>
<exclusion>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
</exclusion>
<exclusion>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- PF4J -->
<dependency>
<groupId>org.pf4j</groupId>
<artifactId>pf4j-spring</artifactId>
<version>${pf4j-spring.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Vert.x -->
<dependency>
<groupId>io.vertx</groupId>
@ -719,16 +669,141 @@
<version>${mqtt.version}</version>
</dependency>
<!-- 专属于 JDK8 安全漏洞升级 -->
<!-- OkHttp -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${logback.version}</version>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<version>${okhttp.version}</version>
<scope>test</scope>
</dependency>
<!-- CoAP - Eclipse Californium -->
<dependency>
<groupId>org.eclipse.californium</groupId>
<artifactId>californium-core</artifactId>
<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>
<artifactId>s3</artifactId>
<version>${awssdk.version}</version>
</dependency>
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
<version>${justauth.version}</version>
</dependency>
<dependency>
<groupId>com.xkcoding.justauth</groupId>
<artifactId>justauth-spring-boot-starter</artifactId>
<version>${justauth-starter.version}</version>
<exclusions>
<!-- 移除,避免和项目里的 hutool-all 冲突 -->
<exclusion>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>${alipay-sdk-java.version}</version>
<exclusions>
<exclusion>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</exclusion>
</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>
<version>${weixin-java.version}</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
<version>${weixin-java.version}</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
<version>${weixin-java.version}</version>
</dependency>
<!-- 积木报表-->
<dependency>
<groupId>org.jeecgframework.jimureport</groupId>
<artifactId>jimureport-spring-boot-starter</artifactId>
<version>${jimureport.version}</version>
</dependency>
<dependency>
<groupId>org.jeecgframework.jimureport</groupId>
<artifactId>jimubi-spring-boot-starter</artifactId>
<version>${jimubi.version}</version>
<exclusions>
<exclusion>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
</exclusion>
<exclusion>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</dependencyManagement>

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

@ -27,10 +27,10 @@ public class PageParam implements Serializable {
@Min(value = 1, message = "页码最小值为 1")
private Integer pageNo = PAGE_NO;
@Schema(description = "每页条数,最大值为 100", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
@Schema(description = "每页条数,最大值为 200", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
@NotNull(message = "每页条数不能为空")
@Min(value = 1, message = "每页条数最小值为 1")
@Max(value = 100, message = "每页条数最大值为 100")
@Max(value = 200, message = "每页条数最大值为 200")
private Integer pageSize = PAGE_SIZE;
}

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

@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.common.core.KeyValue;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@ -65,4 +66,47 @@ public class MapUtils {
return map;
}
/**
* Map BigDecimal
*
* @param map Map
* @param key
* @return BigDecimal null null
*/
public static BigDecimal getBigDecimal(Map<String, ?> map, String key) {
return getBigDecimal(map, key, null);
}
/**
* Map BigDecimal
*
* @param map Map
* @param key
* @param defaultValue
* @return BigDecimal null
*/
public static BigDecimal getBigDecimal(Map<String, ?> map, String key, BigDecimal defaultValue) {
if (map == null) {
return defaultValue;
}
Object value = map.get(key);
if (value == null) {
return defaultValue;
}
if (value instanceof BigDecimal) {
return (BigDecimal) value;
}
if (value instanceof Number) {
return BigDecimal.valueOf(((Number) value).doubleValue());
}
if (value instanceof String) {
try {
return new BigDecimal((String) value);
} catch (NumberFormatException e) {
return defaultValue;
}
}
return defaultValue;
}
}

View File

@ -236,6 +236,23 @@ public class LocalDateTimeUtils {
return LocalDateTime.now().with(TemporalAdjusters.firstDayOfYear()).with(LocalTime.MIN);
}
/**
* N 0
* <p>
* getLatestDays(3) [ 00:00, 00:00, 00:00]
*
* @param days
* @return LocalDateTime
*/
public static List<LocalDateTime> getLatestDays(int days) {
LocalDateTime today = getToday();
List<LocalDateTime> dates = new ArrayList<>(days);
for (int i = days - 1; i >= 0; i--) {
dates.add(today.minusDays(i));
}
return dates;
}
public static List<LocalDateTime[]> getDateRangeList(LocalDateTime startTime,
LocalDateTime endTime,
Integer interval) {
@ -302,6 +319,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 +367,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;
@ -11,9 +9,11 @@ import lombok.SneakyThrows;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
import javax.servlet.http.HttpServletRequest;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
@ -37,14 +37,88 @@ public class HttpUtils {
return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
}
@SuppressWarnings("unchecked")
/**
* URL query parameter
* + query parameter URL path
*
* @see #decodeUrlPath(String)
* @param value
* @return
*/
@SneakyThrows
public static String decodeUtf8(String value) {
return URLDecoder.decode(value, StandardCharsets.UTF_8.name());
}
/**
* URL
* {@link #decodeUtf8(String)} + +
* URL path
*
* @param path URL
* @return
*/
@SneakyThrows
public static String decodeUrlPath(String path) {
if (StrUtil.isEmpty(path)) {
return path;
}
// 先将 + 替换为 %2B避免被 URLDecoder 解码为空格
String encoded = path.replace("+", "%2B");
return URLDecoder.decode(encoded, StandardCharsets.UTF_8.name());
}
/**
* URL /
*
* @param path URL 20250602/xxx.pdf
* @return
*/
public static String encodeUrlPath(String path) {
if (StrUtil.isEmpty(path)) {
return path;
}
String[] segments = path.split(StrUtil.SLASH, -1);
StringBuilder result = new StringBuilder(path.length());
for (int i = 0; i < segments.length; i++) {
if (i > 0) {
result.append(StrUtil.SLASH);
}
result.append(encodeUrlPathSegment(segments[i]));
}
return result.toString();
}
/**
* URL
*
* @param segment URL
* @return
*/
public static String encodeUrlPathSegment(String segment) {
return UriUtils.encodePathSegment(segment, StandardCharsets.UTF_8);
}
public static String removeUrlPathQueryAndFragment(String path) {
if (StrUtil.isEmpty(path)) {
return path;
}
int endIndex = path.length();
int queryIndex = path.indexOf('?');
if (queryIndex >= 0) {
endIndex = queryIndex;
}
int fragmentIndex = path.indexOf('#');
if (fragmentIndex >= 0 && fragmentIndex < endIndex) {
endIndex = fragmentIndex;
}
return path.substring(0, endIndex);
}
public static String replaceUrlQuery(String url, String key, String value) {
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();
}
@ -181,4 +255,14 @@ public class HttpUtils {
}
}
/**
* WebSocket URL HTTP URLws:// → http://wss:// → https://;其它格式原样保留
*
* @param url URL
* @return URL
*/
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,41 @@ public class JsonUtils {
}
}
/**
* JSON Map null
*
* @param text JSON
* @return Map
*/
public static Map<String, Object> parseMap(String text) {
if (StrUtil.isEmpty(text)) {
return null;
}
try {
return objectMapper.readValue(text, new TypeReference<Map<String, Object>>() {});
} catch (IOException e) {
return null;
}
}
/**
* JSON null
*
* @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 +253,14 @@ public class JsonUtils {
}
}
public static String getText(JsonNode node, String fieldName) {
if (node == null) {
return null;
}
JsonNode value = node.get(fieldName);
return value != null && !value.isNull() ? value.asText() : null;
}
public static boolean isJson(String text) {
return JSONUtil.isTypeJSON(text);
}
@ -229,4 +273,53 @@ public class JsonUtils {
return JSONUtil.isTypeJSONObject(str);
}
/**
* Object
* <p>
* jsonString parseObject
*
* @param obj MapPOJO
* @param clazz
* @return
*/
public static <T> T convertObject(Object obj, Class<T> clazz) {
if (obj == null) {
return null;
}
if (clazz.isInstance(obj)) {
return clazz.cast(obj);
}
return objectMapper.convertValue(obj, clazz);
}
/**
* Object
*
* @param obj
* @param typeReference
* @return
*/
public static <T> T convertObject(Object obj, TypeReference<T> typeReference) {
if (obj == null) {
return null;
}
return objectMapper.convertValue(obj, typeReference);
}
/**
* Object List
* <p>
* jsonString parseArray
*
* @param obj List
* @param clazz
* @return List
*/
public static <T> List<T> convertList(Object obj, Class<T> clazz) {
if (obj == null) {
return new ArrayList<>();
}
return objectMapper.convertValue(obj, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
}
}

View File

@ -1,26 +1,85 @@
package cn.iocoder.yudao.framework.common.util.json.databind;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* LocalDateTime
*
* @author
*/
@Slf4j
public class TimestampLocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
public static final TimestampLocalDateTimeSerializer INSTANCE = new TimestampLocalDateTimeSerializer();
private static final Map<Class<?>, Map<String, Field>> FIELD_CACHE = new ConcurrentHashMap<>();
@Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
// 将 LocalDateTime 对象,转换为 Long 时间戳
// 情况一:有 JsonFormat 自定义注解则使用它。https://github.com/YunaiV/ruoyi-vue-pro/pull/1019
String fieldName = gen.getOutputContext().getCurrentName();
if (fieldName != null) {
Object currentValue = gen.getOutputContext().getCurrentValue();
if (currentValue != null) {
Class<?> clazz = currentValue.getClass();
Map<String, Field> fieldMap = FIELD_CACHE.computeIfAbsent(clazz, this::buildFieldMap);
Field field = fieldMap.get(fieldName);
// 进一步修复https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1480
if (field != null && field.isAnnotationPresent(JsonFormat.class)) {
JsonFormat jsonFormat = field.getAnnotation(JsonFormat.class);
try {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(jsonFormat.pattern());
gen.writeString(formatter.format(value));
return;
} catch (Exception ex) {
log.warn("[serialize][({}#{}) 使用 JsonFormat pattern 失败,尝试使用默认的 Long 时间戳]",
clazz.getName(), fieldName, ex);
}
}
}
}
// 情况二:默认将 LocalDateTime 对象,转换为 Long 时间戳
gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
}
/**
*
*
* @param clazz
* @return
*/
private Map<String, Field> buildFieldMap(Class<?> clazz) {
Map<String, Field> fieldMap = new HashMap<>();
for (Field field : ReflectUtil.getFields(clazz)) {
String fieldName = field.getName();
JsonProperty jsonProperty = field.getAnnotation(JsonProperty.class);
if (jsonProperty != null) {
String value = jsonProperty.value();
if (StrUtil.isNotEmpty(value) && ObjUtil.notEqual("\u0000", value)) {
fieldName = value;
}
}
fieldMap.put(fieldName, field);
}
return fieldMap;
}
}

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,25 @@ public class StrUtils {
.collect(Collectors.joining("\n"));
}
/**
* 便 / /
*
* "lao zhang"ZhangSan "zhangsan"
* / / null
*
* hutool-extra {@link PinyinUtil}
* pinyin4j / TinyPinyin / Bopomofo4j NoClassDefFoundError
*
* @param str
* @return
*/
public static String toPinyin(String str) {
if (StrUtil.isBlank(str)) {
return null;
}
return PinyinUtil.getPinyin(str);
}
/**
*
*

View File

@ -0,0 +1,82 @@
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 testEncodeUrlPath() {
// 准备参数
String path = "avatar/中文 100%+文件.jpg";
// 调用
String result = HttpUtils.encodeUrlPath(path);
// 断言
assertEquals("avatar/%E4%B8%AD%E6%96%87%20100%25+%E6%96%87%E4%BB%B6.jpg", result);
}
@Test
public void testDecodeUrlPath() {
// 准备参数:+ 是路径字符,不应该按 query parameter 语义解码为空格
String path = "avatar/%E4%B8%AD%E6%96%87%20100%25+%E6%96%87%E4%BB%B6.jpg";
// 调用
String result = HttpUtils.decodeUrlPath(path);
// 断言
assertEquals("avatar/中文 100%+文件.jpg", result);
}
@Test
public void testRemoveUrlPathQueryAndFragment() {
assertEquals("avatar/test.jpg", HttpUtils.removeUrlPathQueryAndFragment("avatar/test.jpg?token=1#preview"));
assertEquals("avatar/test.jpg", HttpUtils.removeUrlPathQueryAndFragment("avatar/test.jpg#preview?token=1"));
}
@Test
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

@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionContextHolder;
import com.fhs.trans.service.impl.SimpleTransService;
import lombok.RequiredArgsConstructor;
import java.util.Collections;
@ -31,32 +32,53 @@ public class DataPermissionRuleFactoryImpl implements DataPermissionRuleFactory
@Override // mappedStatementId 参数,暂时没有用。以后,可以基于 mappedStatementId + DataPermission 进行缓存
public List<DataPermissionRule> getDataPermissionRule(String mappedStatementId) {
// 1. 无数据权限
// 1.1 无数据权限
if (CollUtil.isEmpty(rules)) {
return Collections.emptyList();
}
// 2. 未配置,则默认开启
// 1.2 未配置,则默认开启
DataPermission dataPermission = DataPermissionContextHolder.get();
if (dataPermission == null) {
return rules;
}
// 3. 已配置,但禁用
// 1.3 已配置,但禁用
if (!dataPermission.enable()) {
return Collections.emptyList();
}
// 1.4 特殊:数据翻译时,强制忽略数据权限 https://github.com/YunaiV/ruoyi-vue-pro/issues/1007
if (isTranslateCall()) {
return Collections.emptyList();
}
// 4. 已配置,只选择部分规则
// 2.1 情况一:已配置,只选择部分规则
if (ArrayUtil.isNotEmpty(dataPermission.includeRules())) {
return rules.stream().filter(rule -> ArrayUtil.contains(dataPermission.includeRules(), rule.getClass()))
.collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询
}
// 5. 已配置,只排除部分规则
// 2.2 已配置,只排除部分规则
if (ArrayUtil.isNotEmpty(dataPermission.excludeRules())) {
return rules.stream().filter(rule -> !ArrayUtil.contains(dataPermission.excludeRules(), rule.getClass()))
.collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询
}
// 6. 已配置,全部规则
// 2.3 已配置,全部规则
return rules;
}
/**
* {@link com.fhs.core.trans.anno.Trans}
*
* easy-trans
*
* @return
*/
private boolean isTranslateCall() {
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
for (StackTraceElement e : stack) {
if (SimpleTransService.class.getName().equals(e.getClassName())) {
return true;
}
}
return false;
}
}

View File

@ -57,8 +57,6 @@ public class DeptDataPermissionRule implements DataPermissionRule {
private static final String DEPT_COLUMN_NAME = "dept_id";
private static final String USER_COLUMN_NAME = "user_id";
static final Expression EXPRESSION_NULL = new NullValue();
private final PermissionCommonApi permissionApi;
/**
@ -120,7 +118,7 @@ public class DeptDataPermissionRule implements DataPermissionRule {
// 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限
if (CollUtil.isEmpty(deptDataPermission.getDeptIds())
&& Boolean.FALSE.equals(deptDataPermission.getSelf())) {
&& Boolean.FALSE.equals(deptDataPermission.getSelf())) {
return new EqualsTo(null, null); // WHERE null = null可以保证返回的数据为空
}
@ -133,7 +131,7 @@ public class DeptDataPermissionRule implements DataPermissionRule {
JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission));
// throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空",
// loginUser.getId(), tableName, tableAlias.getName()));
return EXPRESSION_NULL;
return new EqualsTo(null, null); // WHERE null = null可以保证返回的数据为空
}
if (deptExpression == null) {
return userExpression;

View File

@ -3,12 +3,12 @@ package cn.iocoder.yudao.framework.datapermission.core.rule.dept;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi;
import cn.iocoder.yudao.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import org.junit.jupiter.api.BeforeEach;
@ -20,7 +20,6 @@ import org.mockito.MockedStatic;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRule.EXPRESSION_NULL;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
import static org.junit.jupiter.api.Assertions.*;
@ -151,7 +150,7 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest {
// 调用
Expression expression = rule.getExpression(tableName, tableAlias);
// 断言
assertSame(EXPRESSION_NULL, expression);
assertEquals("null = null", expression.toString());
assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
}
}

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

@ -1,6 +1,5 @@
package cn.iocoder.yudao.framework.tenant.config;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.biz.system.tenant.TenantCommonApi;
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
@ -23,6 +22,7 @@ import cn.iocoder.yudao.framework.web.config.WebProperties;
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import javax.annotation.Resource;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@ -30,7 +30,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.BatchStrategies;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
@ -45,11 +44,7 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.pattern.PathPattern;
import javax.annotation.Resource;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.*;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@ -63,13 +58,6 @@ public class YudaoTenantAutoConfiguration {
@Bean
public TenantFrameworkService tenantFrameworkService(TenantCommonApi tenantApi) {
// 参见 https://gitee.com/zhijiantianya/yudao-cloud/issues/IC6YZF
try {
TenantCommonApi tenantApiImpl = SpringUtil.getBean("tenantApiImpl", TenantCommonApi.class);
if (tenantApiImpl != null) {
tenantApi = tenantApiImpl;
}
} catch (Exception ignored) {}
return new TenantFrameworkServiceImpl(tenantApi);
}
@ -167,20 +155,9 @@ public class YudaoTenantAutoConfiguration {
// ========== MQ ==========
/**
* Redis
*
* TenantRedisMessageInterceptor Bean RedisMessageInterceptor
*/
@Configuration
@ConditionalOnClass(name = "cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate")
public static class TenantRedisMQAutoConfiguration {
@Bean
public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() {
return new TenantRedisMessageInterceptor();
}
@Bean
public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() {
return new TenantRedisMessageInterceptor();
}
@Bean
@ -198,7 +175,6 @@ public class YudaoTenantAutoConfiguration {
// ========== Job ==========
@Bean
@ConditionalOnClass(name = "com.xxl.job.core.handler.annotation.XxlJob")
public TenantJobAspect tenantJobAspect(TenantFrameworkService tenantFrameworkService) {
return new TenantJobAspect(tenantFrameworkService);
}
@ -216,7 +192,12 @@ public class YudaoTenantAutoConfiguration {
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory,
BatchStrategies.scan(yudaoCacheProperties.getRedisScanBatchSize()));
// 创建 TenantRedisCacheManager 对象
return new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration, tenantProperties.getIgnoreCaches());
TenantRedisCacheManager cacheManager = new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration,
tenantProperties.getIgnoreCaches());
// 开启事务感知:@Transactional 方法内的 @CacheEvict / @CachePut 自动延迟到 afterCommit
// 避免事务未提交就清缓存被并发读穿写脏值;无事务时立即生效,行为不变
cacheManager.setTransactionAware(true);
return cacheManager;
}
}

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.framework.tenant.core.redis;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.redis.core.TimeoutRedisCacheManager;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import lombok.extern.slf4j.Slf4j;
@ -21,6 +22,8 @@ import java.util.Set;
@Slf4j
public class TenantRedisCacheManager extends TimeoutRedisCacheManager {
private static final String SPLIT = "#";
private final Set<String> ignoreCaches;
public TenantRedisCacheManager(RedisCacheWriter cacheWriter,
@ -32,10 +35,11 @@ public class TenantRedisCacheManager extends TimeoutRedisCacheManager {
@Override
public Cache getCache(String name) {
String[] names = StrUtil.splitToArray(name, SPLIT);
// 如果开启多租户,则 name 拼接租户后缀
if (!TenantContextHolder.isIgnore()
&& TenantContextHolder.getTenantId() != null
&& !CollUtil.contains(ignoreCaches, name)) {
&& !CollUtil.contains(ignoreCaches, names[0])) {
name = name + ":" + TenantContextHolder.getTenantId();
}
@ -43,4 +47,4 @@ public class TenantRedisCacheManager extends TimeoutRedisCacheManager {
return super.getCache(name);
}
}
}

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

@ -2,7 +2,6 @@ package cn.iocoder.yudao.framework.excel.core.util;
import cn.idev.excel.FastExcelFactory;
import cn.idev.excel.converters.longconverter.LongStringConverter;
import cn.idev.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import cn.iocoder.yudao.framework.excel.core.handler.ColumnWidthMatchStyleStrategy;
import cn.iocoder.yudao.framework.excel.core.handler.SelectSheetWriteHandler;
@ -10,6 +9,7 @@ import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
@ -45,9 +45,12 @@ public class ExcelUtils {
}
public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException {
return FastExcelFactory.read(file.getInputStream(), head, null)
.autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
.doReadAllSync();
// 参考 https://t.zsxq.com/zM77F 帖子,增加 try 处理,兼容 windows 场景
try (InputStream inputStream = file.getInputStream()) {
return FastExcelFactory.read(inputStream, head, null)
.autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
.doReadAllSync();
}
}
}

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

@ -12,7 +12,6 @@ import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessag
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Bean;
@ -70,7 +69,8 @@ public class YudaoRedisMQConsumerAutoConfiguration {
public RedisPendingMessageResendJob redisPendingMessageResendJob(List<AbstractRedisStreamMessageListener<?>> listeners,
RedisMQTemplate redisTemplate,
RedissonClient redissonClient) {
return new RedisPendingMessageResendJob(listeners, redisTemplate, redissonClient);
return new RedisPendingMessageResendJob(listeners, redisTemplate, redissonClient,
RedisPendingMessageResendJob.DEFAULT_RESEND_LOCK_KEY);
}
/**
@ -81,7 +81,8 @@ public class YudaoRedisMQConsumerAutoConfiguration {
public RedisStreamMessageCleanupJob redisStreamMessageCleanupJob(List<AbstractRedisStreamMessageListener<?>> listeners,
RedisMQTemplate redisTemplate,
RedissonClient redissonClient) {
return new RedisStreamMessageCleanupJob(listeners, redisTemplate, redissonClient);
return new RedisStreamMessageCleanupJob(listeners, redisTemplate, redissonClient,
RedisStreamMessageCleanupJob.DEFAULT_CLEANUP_LOCK_KEY);
}
/**

View File

@ -23,7 +23,9 @@ import java.util.Objects;
@AllArgsConstructor
public class RedisPendingMessageResendJob {
private static final String LOCK_KEY = "redis:stream:pending-message-resend:lock";
public static final String DEFAULT_RESEND_LOCK_KEY = "redis:stream:pending-message-resend:lock";
public static final String IOT_RESEND_LOCK_KEY = "redis:stream:pending-message-resend:lock:iot";
/**
* 5
@ -36,22 +38,26 @@ public class RedisPendingMessageResendJob {
private final List<AbstractRedisStreamMessageListener<?>> listeners;
private final RedisMQTemplate redisTemplate;
private final RedissonClient redissonClient;
private final String resendLockKey;
/**
* , 35
*/
@Scheduled(cron = "35 * * * * ?")
public void messageResend() {
RLock lock = redissonClient.getLock(LOCK_KEY);
// 尝试加锁
RLock lock = redissonClient.getLock(resendLockKey);
if (lock.tryLock()) {
try {
execute();
} catch (Exception ex) {
log.error("[messageResend][执行异常]", ex);
log.error("[messageResend][执行异常][lockKey={}]", resendLockKey, ex);
} finally {
lock.unlock();
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
} else {
log.debug("[messageResend][未获取到锁,跳过本轮][lockKey={}]", resendLockKey);
}
}

View File

@ -23,7 +23,16 @@ import java.util.List;
@AllArgsConstructor
public class RedisStreamMessageCleanupJob {
private static final String LOCK_KEY = "redis:stream:message-cleanup:lock";
/**
* MQSpring AbstractRedisStreamMessageListener使
*/
public static final String DEFAULT_CLEANUP_LOCK_KEY = "redis:stream:message-cleanup:lock";
/**
* IoT Redis 线使 {@link #DEFAULT_CLEANUP_LOCK_KEY}
* XTRIM Stream
*/
public static final String IOT_CLEANUP_LOCK_KEY = "redis:stream:message-cleanup:lock:iot";
/**
* 10000
@ -33,22 +42,29 @@ public class RedisStreamMessageCleanupJob {
private final List<AbstractRedisStreamMessageListener<?>> listeners;
private final RedisMQTemplate redisTemplate;
private final RedissonClient redissonClient;
/**
* Redisson Bean
*/
private final String cleanupLockKey;
/**
*
*/
@Scheduled(cron = "0 0 * * * ?")
public void cleanup() {
RLock lock = redissonClient.getLock(LOCK_KEY);
// 尝试加锁
RLock lock = redissonClient.getLock(cleanupLockKey);
if (lock.tryLock()) {
try {
execute();
} catch (Exception ex) {
log.error("[cleanup][执行异常]", ex);
log.error("[cleanup][执行异常][lockKey={}]", cleanupLockKey, ex);
} finally {
lock.unlock();
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
} else {
log.debug("[cleanup][未获取到锁,跳过本轮][lockKey={}]", cleanupLockKey);
}
}
@ -59,8 +75,8 @@ public class RedisStreamMessageCleanupJob {
StreamOperations<String, Object, Object> ops = redisTemplate.getRedisTemplate().opsForStream();
listeners.forEach(listener -> {
try {
// 使用 XTRIM 命令清理消息,只保留最近的 MAX_LEN 条消息
Long trimCount = ops.trim(listener.getStreamKey(), MAX_COUNT, true);
// 使用 XTRIM MAXLEN 精确裁剪approximate=false避免 ~ 模式下长期明显高于上限
Long trimCount = ops.trim(listener.getStreamKey(), MAX_COUNT, false);
if (trimCount != null && trimCount > 0) {
log.info("[execute][Stream({}) 清理消息数量({})]", listener.getStreamKey(), trimCount);
}

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

@ -9,7 +9,10 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
@ -56,11 +59,19 @@ public class IdTypeEnvironmentPostProcessor implements EnvironmentPostProcessor
}
public IdType getIdType(ConfigurableEnvironment environment) {
return environment.getProperty(ID_TYPE_KEY, IdType.class);
String value = environment.getProperty(ID_TYPE_KEY);
try {
return StrUtil.isNotBlank(value) ? IdType.valueOf(value) : IdType.NONE;
} catch (IllegalArgumentException ex) {
log.error("[getIdType][无法解析 id-type 配置值({})]", value, ex);
return IdType.NONE;
}
}
public void setIdType(ConfigurableEnvironment environment, IdType idType) {
environment.getSystemProperties().put(ID_TYPE_KEY, idType);
Map<String, Object> map = new HashMap<>();
map.put(ID_TYPE_KEY, idType);
environment.getPropertySources().addFirst(new MapPropertySource("mybatisPlusIdType", map));
log.info("[setIdType][修改 MyBatis Plus 的 idType 为({})]", idType);
}

View File

@ -20,51 +20,49 @@ public enum DbTypeEnum {
/**
* H2
*
* H2 find_in_set
*/
H2(DbType.H2, "H2", ""),
H2(DbType.H2, "H2", "POSITION(',' || CAST(#{value} AS VARCHAR) || ',' IN ',' || #{column} || ',') > 0"),
/**
* MySQL
*/
MY_SQL(DbType.MYSQL, "MySQL", "FIND_IN_SET('#{value}', #{column}) <> 0"),
MY_SQL(DbType.MYSQL, "MySQL", "FIND_IN_SET(#{value}, #{column}) <> 0"),
/**
* Oracle
*/
ORACLE(DbType.ORACLE, "Oracle", "FIND_IN_SET('#{value}', #{column}) <> 0"),
ORACLE(DbType.ORACLE, "Oracle", "INSTR(',' || #{column} || ',', ',' || #{value} || ',') > 0"),
/**
* PostgreSQL
*
* openGauss 使 ProductName PostgreSQL
*/
POSTGRE_SQL(DbType.POSTGRE_SQL,"PostgreSQL", "POSITION('#{value}' IN #{column}) <> 0"),
POSTGRE_SQL(DbType.POSTGRE_SQL, "PostgreSQL", "POSITION(',' || CAST(#{value} AS VARCHAR) || ',' IN ',' || #{column} || ',') > 0"),
/**
* SQL Server
*/
SQL_SERVER(DbType.SQL_SERVER, "Microsoft SQL Server", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"),
SQL_SERVER(DbType.SQL_SERVER, "Microsoft SQL Server", "CHARINDEX(',' + CAST(#{value} AS varchar(255)) + ',', ',' + #{column} + ',') > 0"),
/**
* SQL Server 2005
*/
SQL_SERVER2005(DbType.SQL_SERVER2005, "Microsoft SQL Server 2005", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"),
SQL_SERVER2005(DbType.SQL_SERVER2005, "Microsoft SQL Server 2005", "CHARINDEX(',' + CAST(#{value} AS varchar(255)) + ',', ',' + #{column} + ',') > 0"),
/**
*
*/
DM(DbType.DM, "DM DBMS", "FIND_IN_SET('#{value}', #{column}) <> 0"),
DM(DbType.DM, "DM DBMS", "FIND_IN_SET(#{value}, #{column}) <> 0"),
/**
*
*/
KINGBASE_ES(DbType.KINGBASE_ES, "KingbaseES", "POSITION('#{value}' IN #{column}) <> 0"),
KINGBASE_ES(DbType.KINGBASE_ES, "KingbaseES", "POSITION(',' || CAST(#{value} AS VARCHAR) || ',' IN ',' || #{column} || ',') > 0"),
/**
* OceanBase
*/
OCEAN_BASE(DbType.OCEAN_BASE, "OceanBase", "FIND_IN_SET('#{value}', #{column}) <> 0")
OCEAN_BASE(DbType.OCEAN_BASE, "OceanBase", "FIND_IN_SET(#{value}, #{column}) <> 0")
;
@ -95,7 +93,9 @@ public enum DbTypeEnum {
}
public static String getFindInSetTemplate(DbType dbType) {
return Optional.of(MAP_BY_MP.get(dbType).getFindInSetTemplate())
return Optional.ofNullable(MAP_BY_MP.get(dbType))
.map(DbTypeEnum::getFindInSetTemplate)
.filter(StrUtil::isNotBlank)
.orElseThrow(() -> new IllegalArgumentException("FIND_IN_SET not supported"));
}
}

View File

@ -68,6 +68,29 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
}
/**
*
*
* @param pageParam pageSize {@link PageParam#PAGE_SIZE_NONE}
* @param clazz
* @param lambdaWrapper MyBatis Plus Join
* @param <D>
* @return
*/
default <D> PageResult<D> selectJoinPage(SortablePageParam pageParam, Class<D> clazz, MPJLambdaWrapper<T> lambdaWrapper) {
// 特殊:不分页,直接查询全部
if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) {
List<D> list = selectJoinList(clazz, lambdaWrapper);
return new PageResult<>(list, (long) list.size());
}
// MyBatis Plus Join 查询
IPage<D> mpPage = MyBatisUtils.buildPage(pageParam, pageParam.getSortingFields());
mpPage = selectJoinPage(mpPage, clazz, lambdaWrapper);
// 转换返回
return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
}
default <DTO> PageResult<DTO> selectJoinPage(PageParam pageParam, Class<DTO> resultTypeClass, MPJBaseJoin<T> joinQueryWrapper) {
IPage<DTO> mpPage = MyBatisUtils.buildPage(pageParam);
selectJoinPage(mpPage, resultTypeClass, joinQueryWrapper);
@ -96,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
*
@ -122,6 +170,17 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
return CollUtil.getFirst(list);
}
/**
*
* <p>
* 使 selectOne
*
* @param queryWrapper
* @return null
*/
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

@ -15,6 +15,7 @@ import java.util.function.Consumer;
* <p>
* 1. xxxIfPresent
* 2. SFunction<S, ?> column + <S> , S
*
* @param <T>
*/
public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
@ -26,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);
@ -101,7 +109,6 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
return this;
}
// ========== 重写父类方法,方便链式调用 ==========
@Override
@ -122,6 +129,12 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
return this;
}
@Override
public <X> MPJLambdaWrapperX<T> orderByAsc(SFunction<X, ?> column) {
super.orderByAsc(true, column);
return this;
}
@Override
public MPJLambdaWrapperX<T> last(String lastSql) {
super.last(lastSql);

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,12 @@ 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_]+)*$");
private static final String FIND_IN_SET_VALUE_PLACEHOLDER = "#{value}";
private static final String FIND_IN_SET_COLUMN_PLACEHOLDER = "#{column}";
public static <T> Page<T> buildPage(PageParam pageParam) {
return buildPage(pageParam, null);
}
@ -38,11 +45,15 @@ public class MyBatisUtils {
public static <T> Page<T> buildPage(PageParam pageParam, Collection<SortingField> sortingFields) {
// 页码 + 数量
Page<T> page = new Page<>(pageParam.getPageNo(), pageParam.getPageSize());
page.setOptimizeJoinOfCountSql(false); // 关联 issuehttps://gitee.com/zhijiantianya/yudao-cloud/issues/ID2QLL
// 排序字段
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;
@ -56,23 +67,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());
@ -80,6 +97,22 @@ public class MyBatisUtils {
}
public static boolean isAscOrder(String order) {
return SortingField.ORDER_ASC.equals(order);
}
public static String getOrderDirection(String order) {
return isAscOrder(order) ? "ASC" : "DESC";
}
private static String buildSafeOrderColumn(String field) {
String columnName = StrUtil.toUnderlineCase(field);
if (StrUtil.isEmpty(columnName) || !SAFE_COLUMN_NAME_PATTERN.matcher(columnName).matches()) {
return null;
}
return columnName;
}
/**
*
* MybatisPlusInterceptor
@ -128,15 +161,43 @@ public class MyBatisUtils {
/**
* find_in_set
*
* @param column
* @param value ()
* @param columnName
* @return sql
*/
public static String findInSet(String column, Object value) {
public static String findInSet(String columnName) {
return findInSet(columnName, 0);
}
/**
* find_in_set apply
*
* @param columnName
* @param paramIndex apply
* @return sql
*/
public static String findInSetWithParamIndex(String columnName, int paramIndex) {
return findInSet(columnName, paramIndex);
}
private static String findInSet(String columnName, int paramIndex) {
DbType dbType = JdbcUtils.getDbType();
return findInSet(dbType, columnName, paramIndex);
}
static String findInSet(DbType dbType, String columnName, int paramIndex) {
if (!isSafeColumnName(columnName)) {
throw new IllegalArgumentException("Invalid column name: " + columnName);
}
if (paramIndex < 0) {
throw new IllegalArgumentException("Invalid param index: " + paramIndex);
}
return DbTypeEnum.getFindInSetTemplate(dbType)
.replace("#{column}", column)
.replace("#{value}", StrUtil.toString(value));
.replace(FIND_IN_SET_COLUMN_PLACEHOLDER, columnName)
.replace(FIND_IN_SET_VALUE_PLACEHOLDER, "{" + paramIndex + "}");
}
private static boolean isSafeColumnName(String columnName) {
return StrUtil.isNotEmpty(columnName) && SAFE_COLUMN_NAME_PATTERN.matcher(columnName).matches();
}
/**

View File

@ -0,0 +1,173 @@
package cn.iocoder.yudao.framework.mybatis.core.util;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.SortingField;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* {@link MyBatisUtils}
*/
public class MyBatisUtilsTest {
@Test
public void testBuildPage_sortingFields() {
// 准备参数
PageParam pageParam = new PageParam();
pageParam.setPageNo(2);
pageParam.setPageSize(20);
List<SortingField> sortingFields = Arrays.asList(
new SortingField("userName", SortingField.ORDER_ASC),
new SortingField("u.id", SortingField.ORDER_DESC),
new SortingField("name desc", SortingField.ORDER_DESC));
// 调用
Page<Object> page = MyBatisUtils.buildPage(pageParam, sortingFields);
// 断言
assertEquals(2, page.getCurrent());
assertEquals(20, page.getSize());
assertEquals(2, page.orders().size());
assertOrderItem(page.orders().get(0), "user_name", true);
assertOrderItem(page.orders().get(1), "u.id", false);
}
@Test
public void testAddOrder_queryWrapper() {
// 准备参数
QueryWrapper<Object> query = new QueryWrapper<>();
List<SortingField> sortingFields = Arrays.asList(
new SortingField("userName", SortingField.ORDER_ASC),
new SortingField("u.id", SortingField.ORDER_DESC),
new SortingField("name;drop", SortingField.ORDER_ASC));
// 调用
MyBatisUtils.addOrder(query, sortingFields);
// 断言
assertEquals(" ORDER BY user_name ASC,u.id DESC", query.getSqlSegment());
}
@Test
public void testAddOrder_lambdaQueryWrapper() {
// 准备参数
LambdaQueryWrapper<Object> query = new LambdaQueryWrapper<>();
List<SortingField> sortingFields = Arrays.asList(
new SortingField("userName", SortingField.ORDER_ASC),
new SortingField("u.id", SortingField.ORDER_DESC),
new SortingField("name`", SortingField.ORDER_ASC));
// 调用
MyBatisUtils.addOrder(query, sortingFields);
// 断言
assertEquals(" ORDER BY user_name ASC, u.id DESC", query.getSqlSegment());
}
@Test
public void testAddOrder_lambdaQueryWrapper_invalidSortingFields() {
// 准备参数
LambdaQueryWrapper<Object> query = new LambdaQueryWrapper<>();
List<SortingField> sortingFields = Arrays.asList(
new SortingField("name desc", SortingField.ORDER_ASC),
new SortingField("name;drop", SortingField.ORDER_DESC));
// 调用
MyBatisUtils.addOrder(query, sortingFields);
// 断言
assertEquals("", query.getSqlSegment());
}
@Test
public void testOrderDirection() {
assertTrue(MyBatisUtils.isAscOrder(SortingField.ORDER_ASC));
assertFalse(MyBatisUtils.isAscOrder(SortingField.ORDER_DESC));
assertEquals("ASC", MyBatisUtils.getOrderDirection(SortingField.ORDER_ASC));
assertEquals("DESC", MyBatisUtils.getOrderDirection(SortingField.ORDER_DESC));
assertEquals("DESC", MyBatisUtils.getOrderDirection(null));
}
@Test
public void testFindInSet() {
assertEquals("FIND_IN_SET({0}, websites) <> 0",
MyBatisUtils.findInSet(DbType.MYSQL, "websites", 0));
assertEquals("POSITION(',' || CAST({0} AS VARCHAR) || ',' IN ',' || websites || ',') > 0",
MyBatisUtils.findInSet(DbType.H2, "websites", 0));
assertEquals("INSTR(',' || t.websites || ',', ',' || {0} || ',') > 0",
MyBatisUtils.findInSet(DbType.ORACLE, "t.websites", 0));
assertEquals("POSITION(',' || CAST({1} AS VARCHAR) || ',' IN ',' || websites || ',') > 0",
MyBatisUtils.findInSet(DbType.POSTGRE_SQL, "websites", 1));
assertEquals("CHARINDEX(',' + CAST({2} AS varchar(255)) + ',', ',' + websites + ',') > 0",
MyBatisUtils.findInSet(DbType.SQL_SERVER, "websites", 2));
}
@Test
public void testFindInSet_invalidColumnName() {
assertThrows(IllegalArgumentException.class,
() -> MyBatisUtils.findInSet(DbType.MYSQL, "websites;drop table system_tenant", 0));
assertThrows(IllegalArgumentException.class,
() -> MyBatisUtils.findInSet(DbType.MYSQL, "FIND_IN_SET(value, websites)", 0));
}
@Test
public void testFindInSet_invalidParamIndex() {
assertThrows(IllegalArgumentException.class,
() -> MyBatisUtils.findInSet(DbType.MYSQL, "websites", -1));
}
@Test
public void testFindInSet_applyBindsValue() {
// 准备参数
QueryWrapper<Object> query = new QueryWrapper<>();
String value = "test' OR 1 = 1";
// 调用
query.apply(MyBatisUtils.findInSet(DbType.MYSQL, "to_mails", 0), value);
// 断言SQL 片段里只有 MyBatis Plus 参数占位,用户输入不会被直接拼接进去
assertEquals("(FIND_IN_SET(#{ew.paramNameValuePairs.MPGENVAL1}, to_mails) <> 0)",
query.getSqlSegment());
assertFalse(query.getSqlSegment().contains(value));
assertEquals(value, query.getParamNameValuePairs().get("MPGENVAL1"));
}
@Test
public void testFindInSet_applyBindsMultipleValues() {
// 准备参数
QueryWrapper<Object> query = new QueryWrapper<>();
String value1 = "1' OR 1 = 1";
String value2 = "2' OR 1 = 1";
// 调用
query.apply(MyBatisUtils.findInSet(DbType.MYSQL, "tag_ids", 0)
+ " OR " + MyBatisUtils.findInSet(DbType.MYSQL, "tag_ids", 1), value1, value2);
// 断言:多个参数都由 MyBatis Plus 生成占位符,不拼接用户输入
assertEquals("(FIND_IN_SET(#{ew.paramNameValuePairs.MPGENVAL1}, tag_ids) <> 0"
+ " OR FIND_IN_SET(#{ew.paramNameValuePairs.MPGENVAL2}, tag_ids) <> 0)",
query.getSqlSegment());
assertFalse(query.getSqlSegment().contains(value1));
assertFalse(query.getSqlSegment().contains(value2));
assertEquals(value1, query.getParamNameValuePairs().get("MPGENVAL1"));
assertEquals(value2, query.getParamNameValuePairs().get("MPGENVAL2"));
}
private void assertOrderItem(OrderItem orderItem, String column, boolean asc) {
assertEquals(column, orderItem.getColumn());
assertEquals(asc, orderItem.isAsc());
}
}

View File

@ -75,8 +75,12 @@ public class YudaoCacheAutoConfiguration {
RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory());
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory,
BatchStrategies.scan(yudaoCacheProperties.getRedisScanBatchSize()));
// 创建 TenantRedisCacheManager 对象
return new TimeoutRedisCacheManager(cacheWriter, redisCacheConfiguration);
// 创建 TimeoutRedisCacheManager 对象
TimeoutRedisCacheManager cacheManager = new TimeoutRedisCacheManager(cacheWriter, redisCacheConfiguration);
// 开启事务感知:@Transactional 方法内的 @CacheEvict / @CachePut 自动延迟到 afterCommit
// 避免事务未提交就清缓存被并发读穿写脏值;无事务时立即生效,行为不变
cacheManager.setTransactionAware(true);
return cacheManager;
}
}

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

@ -42,15 +42,16 @@ public class ApiEncryptResponseWrapper extends HttpServletResponseWrapper {
this.flushBuffer();
byte[] body = byteArrayOutputStream.toByteArray();
// 2. 加密 body
String encryptedBody = symmetricEncryptor != null ? symmetricEncryptor.encryptBase64(body)
: asymmetricEncryptor.encryptBase64(body, KeyType.PublicKey);
response.getWriter().write(encryptedBody);
// 3. 添加加密 header 标识
// 2. 添加加密 header 标识
this.addHeader(properties.getHeader(), "true");
// 特殊特殊https://juejin.cn/post/6867327674675625992
this.addHeader("Access-Control-Expose-Headers", properties.getHeader());
// 3.1 加密 body
String encryptedBody = symmetricEncryptor != null ? symmetricEncryptor.encryptBase64(body)
: asymmetricEncryptor.encryptBase64(body, KeyType.PublicKey);
// 3.2 输出加密后的 body设置 header 要放在 response 的 write 之前)
response.getWriter().write(encryptedBody);
}
@Override

View File

@ -20,6 +20,8 @@ import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.Order;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@ -32,7 +34,9 @@ import javax.servlet.Filter;
import java.util.Map;
import java.util.function.Predicate;
@AutoConfiguration
@AutoConfiguration(beforeName = {
"com.fhs.trans.config.TransServiceConfig" // cloud 独有避免一键改包后RestTemplate 初始化的冲突。可见 https://t.zsxq.com/T4yj7 帖子
})
@EnableConfigurationProperties(WebProperties.class)
public class YudaoWebAutoConfiguration {
@ -81,6 +85,7 @@ public class YudaoWebAutoConfiguration {
}
@Bean
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogCommonApi apiErrorLogApi) {
return new GlobalExceptionHandler(applicationName, apiErrorLogApi);
}
@ -103,6 +108,7 @@ public class YudaoWebAutoConfiguration {
* CorsFilter Bean
*/
@Bean
@Order(value = WebFilterOrderEnum.CORS_FILTER) // 特殊:修复因执行顺序影响到跨域配置不生效问题
public FilterRegistrationBean<CorsFilter> corsFilterBean() {
// 创建 CorsConfiguration 对象
CorsConfiguration config = new CorsConfiguration();
@ -146,9 +152,20 @@ public class YudaoWebAutoConfiguration {
*/
@Bean
@ConditionalOnMissingBean
@LoadBalanced
@Primary
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder.build();
}
/**
* RestTemplate
*
* @param restTemplateBuilder {@link RestTemplateAutoConfiguration#restTemplateBuilder}
*/
@Bean
@LoadBalanced
public RestTemplate loadBalancedRestTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder.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

@ -76,7 +76,7 @@ public class JsonWebSocketMessageHandler extends TextWebSocketHandler {
Long tenantId = WebSocketFrameworkUtils.getTenantId(session);
TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, messageObj));
} catch (Throwable ex) {
log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload());
log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload(), ex);
}
}

View File

@ -27,9 +27,10 @@ public class LoginUserHandshakeInterceptor implements HandshakeInterceptor {
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) {
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
if (loginUser != null) {
WebSocketFrameworkUtils.setLoginUser(loginUser, attributes);
if (loginUser == null) {
return false;
}
WebSocketFrameworkUtils.setLoginUser(loginUser, attributes);
return true;
}

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

@ -1,54 +0,0 @@
package cn.iocoder.yudao.gateway.filter.cors;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Spring Cloud Gateway 2.x Origin BUG
*
* <a href="https://blog.csdn.net/zimou5581/article/details/90043178" />
*
* @author
*/
@Component
public class CorsResponseHeaderFilter implements GlobalFilter, Ordered {
@Override
public int getOrder() {
// 指定此过滤器位于 NettyWriteResponseFilter 之后
// 即待处理完响应体后接着处理响应头
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER + 1;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.defer(() -> {
// https://gitee.com/zhijiantianya/yudao-cloud/pulls/177/
List<String> keysToModify = exchange.getResponse().getHeaders().entrySet().stream()
.filter(kv -> (kv.getValue() != null && kv.getValue().size() > 1))
.filter(kv -> (kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)
|| kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)))
.map(Map.Entry::getKey)
.collect(Collectors.toList());
keysToModify.forEach(key->{
List<String> values = exchange.getResponse().getHeaders().get(key);
if (values != null && !values.isEmpty()) {
exchange.getResponse().getHeaders().put(key, Collections.singletonList(values.get(0)));
}
});
return chain.filter(exchange);
}));
}
}

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

@ -11,4 +11,25 @@ spring:
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
config: # 【注册中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
--- #################### 监控相关配置 ####################
# Actuator 监控端点的配置项
management:
endpoints:
web:
base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator
exposure:
include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。
# Spring Boot Admin 配置项
spring:
boot:
admin:
# Spring Boot Admin Client 客户端的相关配置
client:
instance:
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
username: admin
password: admin

View File

@ -4,16 +4,38 @@ spring:
cloud:
nacos:
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
username: # Nacos 账号
password: # Nacos 密码
username: nacos
password: nacos-admin
discovery: # 【配置中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
config: # 【注册中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUPn
--- #################### 监控相关配置 ####################
# Actuator 监控端点的配置项
management:
endpoints:
web:
base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator
exposure:
include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。
# Spring Boot Admin 配置项
spring:
boot:
admin:
# Spring Boot Admin Client 客户端的相关配置
client:
instance:
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
username: admin
password: admin
# 日志文件配置
logging:
level:
org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO 芋艿先禁用Spring Boot 3.X 存在部分错误的 WARN 提示

View File

@ -192,8 +192,31 @@ 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 数组
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
server:
port: 48080
@ -249,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

@ -0,0 +1,53 @@
package cn.iocoder.yudao.module.ai.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* AI
*
* @author runzhen
*/
@AllArgsConstructor
@Getter
public enum AiDocumentSplitStrategyEnum {
/**
*
*/
AUTO("auto", "自动识别"),
/**
* Token
*/
TOKEN("token", "Token 切分"),
/**
*
*/
PARAGRAPH("paragraph", "段落切分"),
/**
* Markdown QA
*
*
*/
MARKDOWN_QA("markdown_qa", "Markdown QA 切分"),
/**
*
*
*/
SEMANTIC("semantic", "语义切分");
/**
*
*/
private final String code;
/**
*
*/
private final String name;
}

View File

@ -26,7 +26,7 @@ public enum AiPlatformEnum implements ArrayValuable<String> {
HUN_YUAN("HunYuan", "混元"), // 腾讯
SILICON_FLOW("SiliconFlow", "硅基流动"), // 硅基流动
MINI_MAX("MiniMax", "MiniMax"), // 稀宇科技
MOONSHOT("Moonshot", "月之暗"), // KIMI
MOONSHOT("Moonshot", "月之暗"), // KIMI
BAI_CHUAN("BaiChuan", "百川智能"), // 百川智能
// ========== 国外平台 ==========
@ -40,6 +40,7 @@ public enum AiPlatformEnum implements ArrayValuable<String> {
STABLE_DIFFUSION("StableDiffusion", "StableDiffusion"), // Stability AI
MIDJOURNEY("Midjourney", "Midjourney"), // Midjourney
SUNO("Suno", "Suno"), // Suno AI
GROK("Grok","Grok"), // Grok
;

View File

@ -19,9 +19,10 @@
国外OpenAI、Ollama、Midjourney、StableDiffusion、Suno
</description>
<properties>
<spring-ai.version>1.0.1</spring-ai.version>
<alibaba-ai.version>1.0.0.3</alibaba-ai.version>
<tinyflow.version>1.0.2</tinyflow.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.2.2</alibaba-ai.version>
<tinyflow.version>1.2.6</tinyflow.version>
</properties>
<dependencies>
@ -172,7 +173,7 @@
<version>1.0.0</version>
</dependency>
<dependency>
<!-- 月之暗 -->
<!-- 月之暗 -->
<groupId>org.springaicommunity</groupId>
<artifactId>moonshot-spring-boot-starter</artifactId>
<version>1.0.0</version>
@ -239,6 +240,12 @@
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
<version>${spring-ai.version}</version>
<exclusions>
<exclusion>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations-jakarta</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<!-- 客户端 -->
@ -262,6 +269,11 @@
<groupId>com.agentsflex</groupId>
<artifactId>agents-flex-store-elasticsearch</artifactId>
</exclusion>
<exclusion>
<!-- 解决 https://t.zsxq.com/pCBZC 问题 -->
<groupId>com.agentsflex</groupId>
<artifactId>agents-flex-search-engine-es</artifactId>
</exclusion>
<exclusion>
<!-- TODO @芋艿:暂时移除 groovy和 iot 冲突 -->
<groupId>org.codehaus.groovy</groupId>

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

@ -11,7 +11,7 @@ import lombok.Data;
public class AiKnowledgeSegmentPageReqVO extends PageParam {
@Schema(description = "文档编号", example = "1")
private Integer documentId;
private Long documentId;
@Schema(description = "分段内容关键字", example = "Java 开发")
private String content;

View File

@ -27,7 +27,7 @@ import java.util.List;
* @since 2024/4/14 17:35
*/
@TableName(value = "ai_chat_message", autoResultMap = true)
@KeySequence("ai_chat_conversation_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@KeySequence("ai_chat_message_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@Builder
@NoArgsConstructor

View File

@ -14,7 +14,7 @@ import lombok.*;
* @author
*/
@TableName("ai_api_key")
@KeySequence("ai_chat_conversation_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@KeySequence("ai_api_key_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@Builder
@NoArgsConstructor

View File

@ -51,8 +51,7 @@ public interface AiKnowledgeSegmentMapper extends BaseMapperX<AiKnowledgeSegment
MPJLambdaWrapper<AiKnowledgeSegmentDO> wrapper = new MPJLambdaWrapperX<AiKnowledgeSegmentDO>()
.selectAs(AiKnowledgeSegmentDO::getDocumentId, AiKnowledgeSegmentProcessRespVO::getDocumentId)
.selectCount(AiKnowledgeSegmentDO::getId, "count")
.select("COUNT(CASE WHEN vector_id > '" + AiKnowledgeSegmentDO.VECTOR_ID_EMPTY
+ "' THEN 1 ELSE NULL END) AS embeddingCount")
.select("COUNT(CASE WHEN vector_id IS NOT NULL AND vector_id <> '" + AiKnowledgeSegmentDO.VECTOR_ID_EMPTY + "' THEN 1 ELSE NULL END) AS embeddingCount")
.in(AiKnowledgeSegmentDO::getDocumentId, documentIds)
.groupBy(AiKnowledgeSegmentDO::getDocumentId);
return selectJoinList(AiKnowledgeSegmentProcessRespVO.class, wrapper);

View File

@ -7,6 +7,7 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactoryImpl;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.grok.GrokChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants;
@ -16,7 +17,9 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatMod
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient;
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient;
import cn.iocoder.yudao.module.ai.tool.method.PersonService;
import io.micrometer.observation.ObservationRegistry;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
@ -34,12 +37,14 @@ import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClie
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties;
import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties;
import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
import java.util.Optional;
/**
* AI
@ -60,6 +65,13 @@ public class AiAutoConfiguration {
return new AiModelFactoryImpl();
}
@Bean
@ConditionalOnMissingBean
public ObservationRegistry observationRegistry() {
// 特殊:兜底有 ObservationRegistry Bean避免相关的 ChatModel 创建报错。相关 issuehttps://t.zsxq.com/CuPu4
return ObservationRegistry.NOOP;
}
// ========== 各种 AI Client 创建 ==========
@Bean
@ -252,6 +264,28 @@ public class AiAutoConfiguration {
return new SunoApi(yudaoAiProperties.getSuno().getBaseUrl());
}
public ChatModel buildGrokChatClient(YudaoAiProperties.Grok properties) {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(GrokChatModel.MODEL_DEFAULT);
}
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
.baseUrl(Optional.ofNullable(properties.getBaseUrl())
.orElse(GrokChatModel.BASE_URL))
.completionsPath(GrokChatModel.COMPLETE_PATH)
.apiKey(properties.getApiKey())
.build())
.defaultOptions(OpenAiChatOptions.builder()
.model(properties.getModel())
.temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens())
.topP(properties.getTopP())
.build())
.toolCallingManager(getToolCallingManager())
.build();
return new DouBaoChatModel(openAiChatModel);
}
// ========== RAG 相关 ==========
@Bean

View File

@ -160,6 +160,20 @@ public class YudaoAiProperties {
}
@Data
public static class Grok {
private String enable;
private String apiKey;
private String baseUrl;
private String model;
private Double temperature;
private Integer maxTokens;
private Double topP;
}
@Data
public static class WebSearch {

View File

@ -87,7 +87,7 @@ import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiImageAutoConfig
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.OllamaEmbeddingModel;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.ai.ollama.api.OllamaEmbeddingOptions;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiEmbeddingModel;
import org.springframework.ai.openai.OpenAiEmbeddingOptions;
@ -178,6 +178,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
return buildGeminiChatModel(apiKey);
case OLLAMA:
return buildOllamaChatModel(url);
case GROK:
return buildGrokChatModel(apiKey,url);
default:
throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
}
@ -436,10 +438,12 @@ public class AiModelFactoryImpl implements AiModelFactory {
* {@link ZhiPuAiChatAutoConfiguration} zhiPuAiChatModel
*/
private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) {
ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey)
: new ZhiPuAiApi(url, apiKey);
ZhiPuAiApi.Builder zhiPuAiApiBuilder = ZhiPuAiApi.builder().apiKey(apiKey);
if (StrUtil.isNotEmpty(url)) {
zhiPuAiApiBuilder.baseUrl(url);
}
ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder().model(ZhiPuAiApi.DEFAULT_CHAT_MODEL).temperature(0.7).build();
return new ZhiPuAiChatModel(zhiPuAiApi, options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE,
return new ZhiPuAiChatModel(zhiPuAiApiBuilder.build(), options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE,
getObservationRegistry().getIfAvailable());
}
@ -586,6 +590,13 @@ public class AiModelFactoryImpl implements AiModelFactory {
return new StabilityAiImageModel(stabilityAiApi);
}
private ChatModel buildGrokChatModel(String apiKey,String url) {
YudaoAiProperties.Grok properties = new YudaoAiProperties.Grok()
.setBaseUrl(url)
.setApiKey(apiKey);
return new AiAutoConfiguration().buildGrokChatClient(properties);
}
// ========== 各种创建 EmbeddingModel 的方法 ==========
/**
@ -601,10 +612,12 @@ public class AiModelFactoryImpl implements AiModelFactory {
* {@link ZhiPuAiEmbeddingAutoConfiguration} zhiPuAiEmbeddingModel
*/
private ZhiPuAiEmbeddingModel buildZhiPuEmbeddingModel(String apiKey, String url, String model) {
ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey)
: new ZhiPuAiApi(url, apiKey);
ZhiPuAiApi.Builder zhiPuAiApiBuilder = ZhiPuAiApi.builder().apiKey(apiKey);
if (StrUtil.isNotEmpty(url)) {
zhiPuAiApiBuilder.baseUrl(url);
}
ZhiPuAiEmbeddingOptions zhiPuAiEmbeddingOptions = ZhiPuAiEmbeddingOptions.builder().model(model).build();
return new ZhiPuAiEmbeddingModel(zhiPuAiApi, MetadataMode.EMBED, zhiPuAiEmbeddingOptions);
return new ZhiPuAiEmbeddingModel(zhiPuAiApiBuilder.build(), MetadataMode.EMBED, zhiPuAiEmbeddingOptions);
}
/**
@ -632,7 +645,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
private OllamaEmbeddingModel buildOllamaEmbeddingModel(String url, String model) {
OllamaApi ollamaApi = OllamaApi.builder().baseUrl(url).build();
OllamaOptions ollamaOptions = OllamaOptions.builder().model(model).build();
OllamaEmbeddingOptions ollamaOptions = OllamaEmbeddingOptions.builder().model(model).build();
return OllamaEmbeddingModel.builder()
.ollamaApi(ollamaApi)
.defaultOptions(ollamaOptions)

View File

@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.grok;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import reactor.core.publisher.Flux;
/**
* Grok {@link ChatModel}
*
*
*/
@Slf4j
@RequiredArgsConstructor
public class GrokChatModel implements ChatModel {
public static final String BASE_URL = "https://api.x.ai";
public static final String COMPLETE_PATH = "/v1/chat/completions";
public static final String MODEL_DEFAULT = "grok-4-fast-reasoning";
/**
* OpenAI
*/
private final ChatModel openAiChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return openAiChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return openAiChatModel.stream(prompt);
}
@Override
public ChatOptions getDefaultOptions() {
return openAiChatModel.getDefaultOptions();
}
}

View File

@ -3,7 +3,8 @@ package cn.iocoder.yudao.module.ai.framework.security.config;
import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer;
import cn.iocoder.yudao.module.infra.enums.ApiConstants;
import jakarta.annotation.Resource;
import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerSseProperties;
import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerStreamableHttpProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@ -18,7 +19,9 @@ import java.util.Optional;
public class SecurityConfiguration {
@Resource
private Optional<McpServerProperties> serverProperties;
private Optional<McpServerSseProperties> mcpServerSseProperties;
@Resource
private Optional<McpServerStreamableHttpProperties> mcpServerStreamableHttpProperties;
@Bean("aiAuthorizeRequestsCustomizer")
public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() {
@ -42,10 +45,12 @@ public class SecurityConfiguration {
registry.requestMatchers(ApiConstants.PREFIX + "/**").permitAll();
// MCP Server
serverProperties.ifPresent(properties -> {
mcpServerSseProperties.ifPresent(properties -> {
registry.requestMatchers(properties.getSseEndpoint()).permitAll();
registry.requestMatchers(properties.getSseMessageEndpoint()).permitAll();
});
mcpServerStreamableHttpProperties.ifPresent(properties ->
registry.requestMatchers(properties.getMcpEndpoint()).permitAll());
}
};

View File

@ -37,7 +37,6 @@ import cn.iocoder.yudao.module.ai.util.AiUtils;
import cn.iocoder.yudao.module.ai.util.FileTypeUtils;
import com.google.common.collect.Maps;
import io.modelcontextprotocol.client.McpSyncClient;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.MessageType;
@ -49,7 +48,7 @@ import org.springframework.ai.chat.model.StreamingChatModel;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.ai.mcp.client.autoconfigure.properties.McpClientCommonProperties;
import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.resolution.ToolCallbackResolver;
import org.springframework.beans.factory.annotation.Autowired;
@ -59,6 +58,8 @@ import reactor.core.publisher.Flux;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@ -231,20 +232,24 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
// 4.3 流式返回
StringBuffer contentBuffer = new StringBuffer();
StringBuffer reasoningContentBuffer = new StringBuffer();
// 防止执行多次知识库和联网搜索
AtomicBoolean firstExecuteFlag = new AtomicBoolean(true);
AtomicReference<List<AiChatMessageRespVO.KnowledgeSegment>> cacheSegments = new AtomicReference<>();
AtomicReference<List<AiWebSearchResponse.WebPage>> cacheWebSearchPages = new AtomicReference<>();
return streamResponse.map(chunk -> {
// 仅首次:返回知识库、联网搜索
List<AiChatMessageRespVO.KnowledgeSegment> segments = null;
List<AiWebSearchResponse.WebPage> webSearchPages = null;
if (StrUtil.isEmpty(contentBuffer)) {
Map<Long, AiKnowledgeDocumentDO> documentMap = TenantUtils.executeIgnore(() ->
knowledgeDocumentService.getKnowledgeDocumentMap(
convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId)));
segments = BeanUtils.toBean(knowledgeSegments, AiChatMessageRespVO.KnowledgeSegment.class, segment -> {
AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId());
segment.setDocumentName(document != null ? document.getName() : null);
});
if (webSearchResponse != null) {
webSearchPages = webSearchResponse.getLists();
if (firstExecuteFlag.compareAndSet(true, false)) { // CAS 操作,确保仅执行一次
Map<Long, AiKnowledgeDocumentDO> documentMap = TenantUtils.executeIgnore(() -> knowledgeDocumentService.getKnowledgeDocumentMap(
convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId)));
cacheSegments.set(BeanUtils.toBean(knowledgeSegments, AiChatMessageRespVO.KnowledgeSegment.class, segment -> {
AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId());
segment.setDocumentName(document != null ? document.getName() : null);
}));
if (webSearchResponse != null) {
cacheWebSearchPages.set(webSearchResponse.getLists());
}
}
}
// 响应结果
@ -261,7 +266,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
.setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class)
.setContent(StrUtil.nullToDefault(newContent, "")) // 避免 null 的 情况
.setReasoningContent(StrUtil.nullToDefault(newReasoningContent, "")) // 避免 null 的 情况
.setSegments(segments).setWebSearchPages(webSearchPages))); // 知识库 + 联网搜索
.setSegments(cacheSegments.get()).setWebSearchPages(cacheWebSearchPages.get()))); // 知识库 + 联网搜索
}).doOnComplete(() -> {
// 忽略租户,因为 Flux 异步无法透传租户
TenantUtils.executeIgnore(() -> chatMessageMapper.updateById(

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

@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
@ -15,8 +16,11 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper;
import cn.iocoder.yudao.module.ai.enums.AiDocumentSplitStrategyEnum;
import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO;
import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO;
import cn.iocoder.yudao.module.ai.service.knowledge.splitter.MarkdownQaSplitter;
import cn.iocoder.yudao.module.ai.service.knowledge.splitter.SemanticTextSplitter;
import cn.iocoder.yudao.module.ai.service.model.AiModelService;
import com.alibaba.cloud.ai.dashscope.rerank.DashScopeRerankOptions;
import com.alibaba.cloud.ai.model.RerankModel;
@ -39,8 +43,7 @@ import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_CONTENT_TOO_LONG;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_NOT_EXISTS;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*;
import static org.springframework.ai.vectorstore.SearchRequest.SIMILARITY_THRESHOLD_ACCEPT_ALL;
/**
@ -95,8 +98,9 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
AiKnowledgeDO knowledgeDO = knowledgeService.validateKnowledgeExists(documentDO.getKnowledgeId());
VectorStore vectorStore = getVectorStoreById(knowledgeDO);
// 2. 文档切片
List<Document> documentSegments = splitContentByToken(content, documentDO.getSegmentMaxTokens());
// 2. 文档切片(使用自动检测策略)
List<Document> documentSegments = splitContentByStrategy(content, documentDO.getSegmentMaxTokens(),
AiDocumentSplitStrategyEnum.AUTO, documentDO.getUrl());
// 3.1 存储切片
List<AiKnowledgeSegmentDO> segmentDOs = convertList(documentSegments, segment -> {
@ -137,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. 查询需要删除的段落
@ -227,6 +244,9 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
// 2. 检索
List<Document> documents = searchDocument(knowledge, reqBO);
if (CollUtil.isEmpty(documents)) {
return ListUtil.empty();
}
// 3.1 段落召回
List<AiKnowledgeSegmentDO> segments = segmentMapper
@ -292,8 +312,10 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
// 1. 读取 URL 内容
String content = knowledgeDocumentService.readUrl(url);
// 2. 文档切片
List<Document> documentSegments = splitContentByToken(content, segmentMaxTokens);
// 2.1 自动检测文档类型并选择策略
AiDocumentSplitStrategyEnum strategy = detectDocumentStrategy(content, url);
// 2.2 文档切片
List<Document> documentSegments = splitContentByStrategy(content, segmentMaxTokens, strategy, url);
// 3. 转换为段落对象
return convertList(documentSegments, segment -> {
@ -330,11 +352,103 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
return getVectorStoreById(knowledge);
}
private static List<Document> splitContentByToken(String content, Integer segmentMaxTokens) {
TextSplitter textSplitter = buildTokenTextSplitter(segmentMaxTokens);
/**
*
*
* @param content
* @param segmentMaxTokens Token
* @param strategy
* @param url URL
* @return
*/
@SuppressWarnings("EnhancedSwitchMigration")
private List<Document> splitContentByStrategy(String content, Integer segmentMaxTokens,
AiDocumentSplitStrategyEnum strategy, String url) {
// 自动检测策略
if (strategy == AiDocumentSplitStrategyEnum.AUTO) {
strategy = detectDocumentStrategy(content, url);
log.info("[splitContentByStrategy][自动检测到文档策略: {}]", strategy.getName());
}
// 根据策略切分
TextSplitter textSplitter;
switch (strategy) {
case MARKDOWN_QA:
textSplitter = new MarkdownQaSplitter(segmentMaxTokens);
break;
case SEMANTIC:
textSplitter = new SemanticTextSplitter(segmentMaxTokens);
break;
case PARAGRAPH:
textSplitter = new SemanticTextSplitter(segmentMaxTokens, 0); // 段落切分,无重叠
break;
case TOKEN:
default:
textSplitter = buildTokenTextSplitter(segmentMaxTokens);
break;
}
// 执行切分
return textSplitter.apply(Collections.singletonList(new Document(content)));
}
/**
*
*
* @param content
* @param url URL
* @return
*/
private AiDocumentSplitStrategyEnum detectDocumentStrategy(String content, String url) {
if (StrUtil.isEmpty(content)) {
return AiDocumentSplitStrategyEnum.TOKEN;
}
// 1. 检测 Markdown QA 格式
if (isMarkdownQaFormat(content, url)) {
return AiDocumentSplitStrategyEnum.MARKDOWN_QA;
}
// 2. 检测普通 Markdown 文档
if (isMarkdownDocument(url)) {
return AiDocumentSplitStrategyEnum.SEMANTIC;
}
// 3. 默认使用语义切分(比 Token 切分更智能)
return AiDocumentSplitStrategyEnum.SEMANTIC;
}
/**
* Markdown QA
* ##
*/
private boolean isMarkdownQaFormat(String content, String url) {
// 文件扩展名判断
if (StrUtil.isNotEmpty(url) && !url.toLowerCase().endsWith(".md")) {
return false;
}
// 统计二级标题数量
long h2Count = content.lines()
.filter(line -> line.trim().startsWith("## "))
.count();
// 要求一:至少包含 2 个二级标题才认为是 QA 格式
if (h2Count < 2) {
return false;
}
// 要求二检查标题占比QA 文档标题行数相对较多),如果二级标题占比超过 10%,认为是 QA 格式
long totalLines = content.lines().count();
double h2Ratio = (double) h2Count / totalLines;
return h2Ratio > 0.1;
}
/**
* Markdown
*/
private boolean isMarkdownDocument(String url) {
return StrUtil.endWithAnyIgnoreCase(url, ".md", ".markdown");
}
/**
* Token
*/
private static TextSplitter buildTokenTextSplitter(Integer segmentMaxTokens) {
return TokenTextSplitter.builder()
.withChunkSize(segmentMaxTokens)

View File

@ -0,0 +1,342 @@
package cn.iocoder.yudao.module.ai.service.knowledge.splitter;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.transformer.splitter.TextSplitter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Markdown QA
*
* <p>
* <ul>
* <li>## </li>
* <li> QA Token </li>
* <li></li>
* <li> Token </li>
* </ul>
*
* @author runzhen
*/
@Slf4j
@SuppressWarnings("SizeReplaceableByIsEmpty")
public class MarkdownQaSplitter extends TextSplitter {
/**
* "## "
*/
private static final Pattern H2_PATTERN = Pattern.compile("^##\\s+(.+)$", Pattern.MULTILINE);
/**
*
*/
private static final String PARAGRAPH_SEPARATOR = "\n\n";
/**
*
*/
private static final Pattern SENTENCE_PATTERN = Pattern.compile("[。!?.!?]\\s*");
/**
* Token
*/
private final int chunkSize;
/**
* Token 1.3
*/
private final TokenEstimator tokenEstimator;
public MarkdownQaSplitter(int chunkSize) {
this.chunkSize = chunkSize;
this.tokenEstimator = new SimpleTokenEstimator();
}
@Override
protected List<String> splitText(String text) {
if (StrUtil.isEmpty(text)) {
return Collections.emptyList();
}
// 解析 QA 对
List<QaPair> qaPairs = parseQaPairs(text);
if (CollUtil.isEmpty(qaPairs)) {
// 如果没有识别到 QA 格式,按段落切分
return fallbackSplit(text);
}
// 处理每个 QA 对
List<String> result = new ArrayList<>();
for (QaPair qaPair : qaPairs) {
result.addAll(splitQaPair(qaPair));
}
return result;
}
/**
* Markdown QA
*
* @param content
* @return QA
*/
private List<QaPair> parseQaPairs(String content) {
// 找到所有二级标题位置
List<QaPair> qaPairs = new ArrayList<>();
List<Integer> headingPositions = new ArrayList<>();
List<String> questions = new ArrayList<>();
Matcher matcher = H2_PATTERN.matcher(content);
while (matcher.find()) {
headingPositions.add(matcher.start());
questions.add(matcher.group(1).trim());
}
if (CollUtil.isEmpty(headingPositions)) {
return qaPairs;
}
// 提取每个 QA 对
for (int i = 0; i < headingPositions.size(); i++) {
int start = headingPositions.get(i);
int end = (i + 1 < headingPositions.size())
? headingPositions.get(i + 1)
: content.length();
String qaText = content.substring(start, end).trim();
String question = questions.get(i);
// 提取答案部分(去掉问题标题)
String answer = qaText.substring(qaText.indexOf('\n') + 1).trim();
qaPairs.add(new QaPair(question, answer, qaText));
}
return qaPairs;
}
/**
* QA
*
* @param qaPair QA
* @return
*/
private List<String> splitQaPair(QaPair qaPair) {
// 如果整个 QA 对不超过限制,保持完整
List<String> chunks = new ArrayList<>();
String fullQa = qaPair.fullText;
int qaTokens = tokenEstimator.estimate(fullQa);
if (qaTokens <= chunkSize) {
chunks.add(fullQa);
return chunks;
}
// 长答案需要切分
log.debug("QA 对超过 Token 限制 ({} > {}),开始智能切分: {}", qaTokens, chunkSize, qaPair.question);
List<String> answerChunks = splitLongAnswer(qaPair.answer, qaPair.question);
for (String answerChunk : answerChunks) {
// 每个片段都包含完整问题
String chunkText = "## " + qaPair.question + "\n" + answerChunk;
chunks.add(chunkText);
}
return chunks;
}
/**
*
*
* @param answer
* @param question
* @return
*/
private List<String> splitLongAnswer(String answer, String question) {
List<String> chunks = new ArrayList<>();
// 预留问题的 Token 空间
String questionHeader = "## " + question + "\n";
int questionTokens = tokenEstimator.estimate(questionHeader);
int availableTokens = chunkSize - questionTokens - 10; // 预留 10 个 Token 的缓冲
// 先按段落切分
String[] paragraphs = answer.split(PARAGRAPH_SEPARATOR);
StringBuilder currentChunk = new StringBuilder();
int currentTokens = 0;
for (String paragraph : paragraphs) {
if (StrUtil.isEmpty(paragraph)) {
continue;
}
int paragraphTokens = tokenEstimator.estimate(paragraph);
// 如果单个段落就超过限制,需要按句子切分
if (paragraphTokens > availableTokens) {
// 先保存当前块
if (currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
currentChunk = new StringBuilder();
currentTokens = 0;
}
// 按句子切分长段落
chunks.addAll(splitLongParagraph(paragraph, availableTokens));
continue;
}
// 如果加上这个段落会超过限制
if (currentTokens + paragraphTokens > availableTokens && currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
currentChunk = new StringBuilder();
currentTokens = 0;
}
if (currentChunk.length() > 0) {
currentChunk.append("\n\n");
}
// 添加段落
currentChunk.append(paragraph);
currentTokens += paragraphTokens;
}
// 添加最后一块
if (currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
}
return CollUtil.isEmpty(chunks) ? Collections.singletonList(answer) : chunks;
}
/**
*
*
* @param paragraph
* @param availableTokens Token
* @return
*/
private List<String> splitLongParagraph(String paragraph, int availableTokens) {
// 按句子切分
List<String> chunks = new ArrayList<>();
String[] sentences = SENTENCE_PATTERN.split(paragraph);
// 按句子累积切分
StringBuilder currentChunk = new StringBuilder();
int currentTokens = 0;
for (String sentence : sentences) {
if (StrUtil.isEmpty(sentence)) {
continue;
}
int sentenceTokens = tokenEstimator.estimate(sentence);
// 如果单个句子就超过限制,强制切分
if (sentenceTokens > availableTokens) {
if (currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
currentChunk = new StringBuilder();
currentTokens = 0;
}
chunks.add(sentence.trim());
continue;
}
// 如果加上这个句子会超过限制
if (currentTokens + sentenceTokens > availableTokens && currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
currentChunk = new StringBuilder();
currentTokens = 0;
}
// 添加句子
currentChunk.append(sentence);
currentTokens += sentenceTokens;
}
// 添加最后一块
if (currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
}
return chunks.isEmpty() ? Collections.singletonList(paragraph) : chunks;
}
/**
* QA
*
* @param content
* @return
*/
private List<String> fallbackSplit(String content) {
// 按段落切分
List<String> chunks = new ArrayList<>();
String[] paragraphs = content.split(PARAGRAPH_SEPARATOR);
// 按段落累积切分
StringBuilder currentChunk = new StringBuilder();
int currentTokens = 0;
for (String paragraph : paragraphs) {
if (StrUtil.isEmpty(paragraph)) {
continue;
}
int paragraphTokens = tokenEstimator.estimate(paragraph);
// 如果加上这个段落会超过限制
if (currentTokens + paragraphTokens > chunkSize && currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
currentChunk = new StringBuilder();
currentTokens = 0;
}
// 添加段落
if (currentChunk.length() > 0) {
currentChunk.append("\n\n");
}
currentChunk.append(paragraph);
currentTokens += paragraphTokens;
}
// 添加最后一块
if (currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
}
return chunks.isEmpty() ? Collections.singletonList(content) : chunks;
}
/**
* QA
*/
@AllArgsConstructor
private static class QaPair {
String question;
String answer;
String fullText;
}
/**
* Token
*/
public interface TokenEstimator {
int estimate(String text);
}
/**
* Token
* 1 1 Token
* 1 1.3 Token
*/
private static class SimpleTokenEstimator implements TokenEstimator {
@Override
public int estimate(String text) {
if (StrUtil.isEmpty(text)) {
return 0;
}
int chineseChars = 0;
int englishWords = 0;
// 简单统计中英文
for (char c : text.toCharArray()) {
if (c >= 0x4E00 && c <= 0x9FA5) {
chineseChars++;
}
}
// 英文单词估算
String[] words = text.split("\\s+");
for (String word : words) {
if (word.matches(".*[a-zA-Z].*")) {
englishWords++;
}
}
return chineseChars + (int) (englishWords * 1.3);
}
}
}

View File

@ -0,0 +1,301 @@
package cn.iocoder.yudao.module.ai.service.knowledge.splitter;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.transformer.splitter.TextSplitter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
*
*
* <p>
* <ul>
* <li></li>
* <li></li>
* <li></li>
* <li></li>
* </ul>
*
* @author runzhen
*/
@Slf4j
public class SemanticTextSplitter extends TextSplitter {
/**
* Token
*/
private final int chunkSize;
/**
*
*/
private final int chunkOverlap;
/**
*
*/
private static final List<String> PARAGRAPH_SEPARATORS = Arrays.asList(
"\n\n\n", // 三个换行
"\n\n", // 双换行
"\n" // 单换行
);
/**
*
*/
private static final Pattern SENTENCE_END_PATTERN = Pattern.compile(
"[。!?.!?]+[\\s\"')】\\]]*"
);
/**
* Token
*/
private final MarkdownQaSplitter.TokenEstimator tokenEstimator;
public SemanticTextSplitter(int chunkSize, int chunkOverlap) {
this.chunkSize = chunkSize;
this.chunkOverlap = Math.min(chunkOverlap, chunkSize / 2); // 重叠不超过一半
this.tokenEstimator = new SimpleTokenEstimator();
}
public SemanticTextSplitter(int chunkSize) {
this(chunkSize, 50); // 默认重叠 50 个 Token
}
@Override
protected List<String> splitText(String text) {
if (StrUtil.isEmpty(text)) {
return Collections.emptyList();
}
return splitTextRecursive(text);
}
/**
*
*
* @param text
* @return
*/
private List<String> splitTextRecursive(String text) {
List<String> chunks = new ArrayList<>();
// 如果文本不超过限制,直接返回
int textTokens = tokenEstimator.estimate(text);
if (textTokens <= chunkSize) {
chunks.add(text.trim());
return chunks;
}
// 尝试按不同分隔符切分
List<String> splits = null;
String usedSeparator = null;
for (String separator : PARAGRAPH_SEPARATORS) {
if (text.contains(separator)) {
splits = Arrays.asList(text.split(Pattern.quote(separator)));
usedSeparator = separator;
break;
}
}
// 如果没有找到段落分隔符,按句子切分
if (splits == null || splits.size() == 1) {
splits = splitBySentences(text);
usedSeparator = ""; // 句子切分不需要分隔符
}
// 合并小片段
chunks = mergeSplits(splits, usedSeparator);
return chunks;
}
/**
*
*
* @param text
* @return
*/
private List<String> splitBySentences(String text) {
// 使用正则表达式匹配句子结束位置
List<String> sentences = new ArrayList<>();
int lastEnd = 0;
Matcher matcher = SENTENCE_END_PATTERN.matcher(text);
while (matcher.find()) {
String sentence = text.substring(lastEnd, matcher.end()).trim();
if (StrUtil.isNotEmpty(sentence)) {
sentences.add(sentence);
}
lastEnd = matcher.end();
}
// 添加剩余部分
if (lastEnd < text.length()) {
String remaining = text.substring(lastEnd).trim();
if (StrUtil.isNotEmpty(remaining)) {
sentences.add(remaining);
}
}
return sentences.isEmpty() ? Collections.singletonList(text) : sentences;
}
/**
*
*
* @param splits
* @param separator
* @return
*/
private List<String> mergeSplits(List<String> splits, String separator) {
List<String> chunks = new ArrayList<>();
List<String> currentChunks = new ArrayList<>();
int currentLength = 0;
for (String split : splits) {
if (StrUtil.isEmpty(split)) {
continue;
}
int splitTokens = tokenEstimator.estimate(split);
// 如果单个片段就超过限制,进一步递归切分
if (splitTokens > chunkSize) {
// 先保存当前累积的块
if (!currentChunks.isEmpty()) {
String chunkText = String.join(separator, currentChunks);
chunks.add(chunkText.trim());
currentChunks.clear();
currentLength = 0;
}
// 递归切分大片段
if (!separator.isEmpty()) {
// 如果是段落分隔符,尝试按句子切分
chunks.addAll(splitTextRecursive(split));
} else {
// 如果已经是句子级别,强制按字符切分
chunks.addAll(forceSplitLongText(split));
}
continue;
}
// 计算加上分隔符的 Token 数
int separatorTokens = StrUtil.isEmpty(separator) ? 0 : tokenEstimator.estimate(separator);
// 如果加上这个片段会超过限制
if (!currentChunks.isEmpty() && currentLength + splitTokens + separatorTokens > chunkSize) {
// 保存当前块
String chunkText = String.join(separator, currentChunks);
chunks.add(chunkText.trim());
// 处理重叠:保留最后几个片段
currentChunks = getOverlappingChunks(currentChunks, separator);
currentLength = estimateTokens(currentChunks, separator);
}
// 添加当前片段
currentChunks.add(split);
currentLength += splitTokens + separatorTokens;
}
// 添加最后一块
if (!currentChunks.isEmpty()) {
String chunkText = String.join(separator, currentChunks);
chunks.add(chunkText.trim());
}
return chunks;
}
/**
*
*
* @param chunks
* @param separator
* @return
*/
private List<String> getOverlappingChunks(List<String> chunks, String separator) {
if (chunkOverlap == 0 || chunks.isEmpty()) {
return new ArrayList<>();
}
// 从后往前取片段,直到达到重叠大小
List<String> overlapping = new ArrayList<>();
int tokens = 0;
for (int i = chunks.size() - 1; i >= 0; i--) {
String chunk = chunks.get(i);
int chunkTokens = tokenEstimator.estimate(chunk);
if (tokens + chunkTokens > chunkOverlap) {
break;
}
// 添加到重叠列表前端
overlapping.add(0, chunk);
tokens += chunkTokens + (StrUtil.isEmpty(separator) ? 0 : tokenEstimator.estimate(separator));
}
return overlapping;
}
/**
* Token
*
* @param chunks
* @param separator
* @return Token
*/
private int estimateTokens(List<String> chunks, String separator) {
int total = 0;
for (int i = 0; i < chunks.size(); i++) {
total += tokenEstimator.estimate(chunks.get(i));
if (i < chunks.size() - 1 && StrUtil.isNotEmpty(separator)) {
total += tokenEstimator.estimate(separator);
}
}
return total;
}
/**
*
*
* @param text
* @return
*/
private List<String> forceSplitLongText(String text) {
List<String> chunks = new ArrayList<>();
int charsPerChunk = (int) (chunkSize * 0.8); // 保守估计
for (int i = 0; i < text.length(); i += charsPerChunk) {
int end = Math.min(i + charsPerChunk, text.length());
String chunk = text.substring(i, end);
chunks.add(chunk.trim());
}
log.warn("文本过长,已强制按字符切分,可能影响语义完整性");
return chunks;
}
/**
* Token
*/
private static class SimpleTokenEstimator implements MarkdownQaSplitter.TokenEstimator {
@Override
public int estimate(String text) {
if (StrUtil.isEmpty(text)) {
return 0;
}
int chineseChars = 0;
int englishWords = 0;
// 简单统计中英文
for (char c : text.toCharArray()) {
if (c >= 0x4E00 && c <= 0x9FA5) {
chineseChars++;
}
}
// 英文单词估算
String[] words = text.split("\\s+");
for (String word : words) {
if (word.matches(".*[a-zA-Z].*")) {
englishWords++;
}
}
return chineseChars + (int) (englishWords * 1.3);
}
}
}

View File

@ -84,7 +84,7 @@ public class UserProfileQueryToolFunction
request.setId(loginUser.getId());
}
return TenantUtils.execute(tenantId, () -> {
AdminUserRespDTO user = adminUserApi.getUser(request.getId()).getCheckedData();
AdminUserRespDTO user = adminUserApi.getUser(request.getId());
return BeanUtils.toBean(user, Response.class);
});
}

View File

@ -1,10 +1,10 @@
package cn.iocoder.yudao.module.ai.tool.method;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.List;
import java.util.Map;

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;
@ -16,7 +18,7 @@ import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.deepseek.DeepSeekAssistantMessage;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.minimax.MiniMaxChatOptions;
import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.ai.ollama.api.OllamaChatOptions;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.zhipuai.ZhiPuAiChatOptions;
@ -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:
@ -68,6 +93,7 @@ public class AiUtils {
case OPENAI:
case GEMINI: // 复用 OpenAI 客户端
case BAI_CHUAN: // 复用 OpenAI 客户端
case GROK: // 复用 OpenAI 客户端
return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case AZURE_OPENAI:
@ -77,7 +103,7 @@ public class AiUtils {
return AnthropicChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case OLLAMA:
return OllamaOptions.builder().model(model).temperature(temperature).numPredict(maxTokens)
return OllamaChatOptions.builder().model(model).temperature(temperature).numPredict(maxTokens)
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
default:
throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
@ -124,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 毫秒
--- #################### 监控相关配置 ####################
@ -123,6 +123,8 @@ spring:
client:
instance:
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
username: admin
password: admin
# Spring Boot Admin Server 服务端的相关配置
context-path: /admin # 配置 Spring

View File

@ -98,7 +98,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
--- #################### 监控相关配置 ####################
@ -118,6 +118,8 @@ spring:
client:
instance:
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
username: admin
password: admin
# 日志文件配置
logging:

View File

@ -144,7 +144,7 @@ spring:
api-key: sk-47aa124781be4bfb95244cc62f6xxxx
minimax: # Minimaxhttps://www.minimaxi.com/
api-key: xxxx
moonshot: # 月之暗KIMI
moonshot: # 月之暗KIMI
api-key: sk-abc
deepseek: # DeepSeek
api-key: sk-e94db327cc7d457d99a8de8810fc6b12
@ -168,6 +168,8 @@ spring:
filesystem:
url: http://127.0.0.1:8089
sse-endpoint: /sse
annotation-scanner:
enabled: false # TODO @芋艿:有 bug https://github.com/spring-projects/spring-ai/issues/4917 需要官方修复
yudao:
ai:

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