mirror of
https://gitee.com/zhijiantianya/ruoyi-vue-pro.git
synced 2026-06-23 08:29:28 +08:00
Compare commits
285 Commits
feature/jd
...
master-jdk
| Author | SHA1 | Date | |
|---|---|---|---|
| f6b5ef06d9 | |||
| adbec22e95 | |||
| f587d4f756 | |||
| 93eabb4efd | |||
| ca367c376f | |||
| 296eb22b68 | |||
| ca815b4c30 | |||
| c779a47661 | |||
| 5d1fd70dc3 | |||
| 57f575bf2e | |||
| bb538ba4f8 | |||
| d9b320f29f | |||
| cd47e9dda6 | |||
| 863a420287 | |||
| cd0156e0e2 | |||
| acda3c95de | |||
| 3d1b66d40b | |||
| 61d312d2e9 | |||
| 7dd3aed542 | |||
| c9392af494 | |||
| 98a977dd1a | |||
| 9cd964a9d8 | |||
| d37bc069a3 | |||
| c7649efa45 | |||
| d97b85bb4c | |||
| eb767f77e3 | |||
| 25c2fbd4bc | |||
| 633bd70dfe | |||
| 08f7c33cd4 | |||
| 0075d19f88 | |||
| 28aeb8a666 | |||
| 93dadc14f6 | |||
| 6d3d097383 | |||
| 93dd97afb7 | |||
| 472993e25a | |||
| 858a351c94 | |||
| 81a77aade1 | |||
| c1101dc4b9 | |||
| 7b6adb410e | |||
| 45422d84ab | |||
| 9b9a10695a | |||
| 11f1a3ee9c | |||
| 11dd407e18 | |||
| a426cc2f4b | |||
| e72c02497f | |||
| 574f90d956 | |||
| f07ec76806 | |||
| 4ca0aadd4a | |||
| f6886a780d | |||
| 6a3b384fc3 | |||
| eddd21f4e8 | |||
| 1d463f4f1a | |||
| 78d798bbe9 | |||
| 83ce168a68 | |||
| 91f356fc74 | |||
| 5e8df3089d | |||
| 45399687b4 | |||
| ef5a3ae7f8 | |||
| dea5e07ed6 | |||
| 115055c403 | |||
| c13ca7b4d8 | |||
| 92c0ea303c | |||
| 3235b4e707 | |||
| 40c9449f39 | |||
| 5998e7f931 | |||
| 11b4eef41d | |||
| 3d0142abe1 | |||
| ec9862cd29 | |||
| bfca0820ca | |||
| ca702b81af | |||
| f1660e18e4 | |||
| 688de72367 | |||
| af9b6253b1 | |||
| abee9ff48d | |||
| eaaeb86a0c | |||
| 89c137a915 | |||
| 0943307785 | |||
| 2f5984afd9 | |||
| 42d4112bd9 | |||
| e03e3c1a89 | |||
| 23c642ed72 | |||
| 4ae3f6b2c9 | |||
| e44011754c | |||
| fe4a774c1e | |||
| 5f2abdabbe | |||
| 8ba906c6eb | |||
| b776dc2a06 | |||
| 215d0ce8f0 | |||
| c6813d43af | |||
| 07f26a7e02 | |||
| c39865e90d | |||
| bd29116e45 | |||
| a06fb9e995 | |||
| 9b44ed74e6 | |||
| 36c30a431a | |||
| 49241c3123 | |||
| f5ef0a8997 | |||
| bda892277d | |||
| bb9a2b5382 | |||
| 513c130151 | |||
| a4b485562f | |||
| dd6be0e595 | |||
| 69121bec6e | |||
| bac7cf17d8 | |||
| 0d6a75a2a6 | |||
| da96ceab7a | |||
| cab59d4dd8 | |||
| f0e4639920 | |||
| 3f1d86efff | |||
| f938362b04 | |||
| b18281fc5c | |||
| 79be419067 | |||
| ee9362a2f2 | |||
| 6bfaa848f2 | |||
| 865a58a646 | |||
| d60156015b | |||
| 032d955800 | |||
| 611880a3c4 | |||
| 6b1a0cfce2 | |||
| 36c4410512 | |||
| 2fd201bf59 | |||
| 9e1a6b15e4 | |||
| 1eda319ea0 | |||
| dc081cfdd2 | |||
| 9ce816e247 | |||
| 70271c1cbf | |||
| c69e20d681 | |||
| 690a549963 | |||
| 27685b99c8 | |||
| bb5ac45be8 | |||
| 8996b94795 | |||
| 07f3bab72a | |||
| d0aac3824d | |||
| b683031528 | |||
| 8b497fa655 | |||
| 530d3b3a5f | |||
| fe77884cc3 | |||
| bcac553c69 | |||
| 348b97047d | |||
| 3d8b8792fd | |||
| 1db025649a | |||
| 012dc01182 | |||
| f360040a3d | |||
| ded120902a | |||
| 3b0abfb26e | |||
| 241064e7d0 | |||
| f5bec3eec6 | |||
| ae71b7134c | |||
| bed0a463f5 | |||
| 8a4ad959d6 | |||
| b320e3c19a | |||
| 9ebf720901 | |||
| 7d5ee9f1c9 | |||
| f640b67159 | |||
| f2136c43d3 | |||
| 28cf87ac8a | |||
| 0d7cb763ff | |||
| 43b5de5192 | |||
| 96ae9bbcb0 | |||
| 14090be672 | |||
| 6e1a8dc343 | |||
| 9d11741aa5 | |||
| 9720f20865 | |||
| ee01a16f8b | |||
| eb2d3c39e2 | |||
| b5f5c408ee | |||
| b0221c21cd | |||
| b51ca45531 | |||
| b0ca5b7550 | |||
| 2804e5ed10 | |||
| ba1a85e38a | |||
| f0be7ba137 | |||
| e266a0fc4a | |||
| f03143c4ee | |||
| cee0688c30 | |||
| 63d00c2bf2 | |||
| 479af56a08 | |||
| bfd374eedf | |||
| 4d2f470499 | |||
| 6e823f848d | |||
| ed9f23ae8c | |||
| 719b139ca2 | |||
| 1c29398da5 | |||
| 37dfd6c07b | |||
| 41762f0ae5 | |||
| 53fc9ae4fc | |||
| ae36d1ad68 | |||
| 5f88c9ca35 | |||
| 45889459b7 | |||
| af6e283f49 | |||
| 782314f17f | |||
| a4901a848d | |||
| fc90635abb | |||
| 37f0a8a01d | |||
| d0fbab8f73 | |||
| 96f2d1a12f | |||
| b0b707ddc0 | |||
| 711a692c3a | |||
| ceb609785b | |||
| 8d1089bac5 | |||
| 3ca5db95e0 | |||
| 8b908afe90 | |||
| 6a10ed7d88 | |||
| 16d5c0b0cc | |||
| 843cc90b87 | |||
| 106cf1c9f7 | |||
| f5b0750899 | |||
| 1896721575 | |||
| 44981bbee8 | |||
| fa474c6f03 | |||
| 84304718dc | |||
| b814a87361 | |||
| ea29dea47c | |||
| 886f4fe1f2 | |||
| cce3dd6110 | |||
| 443d91d7e3 | |||
| e4a6cc932c | |||
| 736a1a2f16 | |||
| 2ab72872be | |||
| e2eab4fe78 | |||
| 15a1bca721 | |||
| affc409022 | |||
| 758dfcbb80 | |||
| ef19ad396e | |||
| 2f084bd5e6 | |||
| 99121a25e0 | |||
| 0ce9eddedb | |||
| bf8eb273ec | |||
| 5e824ccb4e | |||
| 3361c75726 | |||
| 7057f4715b | |||
| 26413e09f3 | |||
| 37cf16c142 | |||
| ab35a13fc4 | |||
| 4ab9569508 | |||
| ca0aadac49 | |||
| e89724e5cd | |||
| f495f9c805 | |||
| 2ce3a99318 | |||
| 04ea1c9db5 | |||
| dc7be58506 | |||
| d6d90b2bd5 | |||
| bee3337925 | |||
| ac05257ec0 | |||
| 6da5846b74 | |||
| 0e3373c553 | |||
| 5ded56853e | |||
| 16e1936c48 | |||
| ba5b7dbd0a | |||
| f6a29e530a | |||
| 1269e626ca | |||
| 0ac1f0cc28 | |||
| 2d277765e7 | |||
| 984312302a | |||
| da92b5a582 | |||
| b49339d08b | |||
| 1f58fd2be4 | |||
| d67362043e | |||
| 29a3ad42b6 | |||
| 40be6ed727 | |||
| 788c24dff4 | |||
| b540f8d46d | |||
| dd272cb54a | |||
| 267477c973 | |||
| 2c5287a9b2 | |||
| f1731e4446 | |||
| dc28c7e8a2 | |||
| bada82f8cc | |||
| 651619d5ef | |||
| fa23ce144d | |||
| 2b891cb432 | |||
| 84cea03752 | |||
| 51e724fc90 | |||
| 4015b2f213 | |||
| 0e79d8ec53 | |||
| 9c1764e36e | |||
| 04123e5987 | |||
| 354fe6fcab | |||
| 9cee1b3ceb | |||
| f694825435 | |||
| ff88d53b3b | |||
| 77f3131ef3 | |||
| 2d052ea752 | |||
| 3696b666f4 | |||
| bb0b7056cf |
1
.gitignore
vendored
1
.gitignore
vendored
@ -51,4 +51,5 @@ rebel.xml
|
||||
application-my.yaml
|
||||
|
||||
/yudao-ui-app/unpackage/
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
|
||||
BIN
.image/common/im-feature.png
Normal file
BIN
.image/common/im-feature.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 169 KiB |
BIN
.image/common/im-preview-home.png
Normal file
BIN
.image/common/im-preview-home.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
BIN
.image/common/im-preview-manager.png
Normal file
BIN
.image/common/im-preview-manager.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
20
README.md
20
README.md
@ -28,8 +28,8 @@
|
||||
| 【完整版】[ruoyi-vue-pro](https://gitee.com/zhijiantianya/ruoyi-vue-pro) | [`master`](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master-jdk17/) 分支 |
|
||||
| 【精简版】[yudao-boot-mini](https://gitee.com/yudaocode/yudao-boot-mini) | [`master`](https://gitee.com/yudaocode/yudao-boot-mini/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/yudaocode/yudao-boot-mini/tree/master-jdk17/) 分支 |
|
||||
|
||||
* 【完整版】:包括系统功能、基础设施、会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、WMS、MES、AI 大模型、IoT 物联网 等功能
|
||||
* 【精简版】:只包括系统功能、基础设施功能,不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、WMS、MES、AI 大模型、IoT 物联网 等功能
|
||||
* 【完整版】:包括系统功能、基础设施、会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、WMS、MES、IM 即时通讯、AI 大模型、IoT 物联网等功能
|
||||
* 【精简版】:只包括系统功能、基础设施功能,不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、WMS、MES、IM 即时通讯、AI 大模型、IoT 物联网等功能
|
||||
|
||||
可参考 [《迁移文档》](https://doc.iocoder.cn/migrate-module/) ,只需要 5-10 分钟,即可将【完整版】按需迁移到【精简版】
|
||||
|
||||
@ -102,7 +102,7 @@
|
||||
|
||||
团队包含专业的项目经理、架构师、前端工程师、后端工程师、测试工程师、运维工程师,可以提供全流程的外包服务。
|
||||
|
||||
项目可以是商城、SCRM 系统、OA 系统、物流系统、ERP 系统、CMS 系统、HIS 系统、支付系统、IM 聊天、微信公众号、微信小程序等等。
|
||||
项目可以是商城、SCRM 系统、OA 系统、物流系统、ERP 系统、CMS 系统、HIS 系统、支付系统、IM 即时通讯、微信公众号、微信小程序等等。
|
||||
|
||||
## 🐼 内置功能
|
||||
|
||||
@ -308,6 +308,19 @@
|
||||
|
||||

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

|
||||
|
||||
| 聊天界面 | 聊天管理 |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
## 🐨 技术栈
|
||||
|
||||
### 模块
|
||||
@ -327,6 +340,7 @@
|
||||
| `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 模块 |
|
||||
|
||||
17
pom.xml
17
pom.xml
@ -26,6 +26,7 @@
|
||||
<!-- <module>yudao-module-iot</module>-->
|
||||
<!-- <module>yudao-module-mes</module>-->
|
||||
<!-- <module>yudao-module-wms</module>-->
|
||||
<!-- <module>yudao-module-im</module>-->
|
||||
<!-- 请参考 https://doc.iocoder.cn/ai/build/ 文档,完成 AI 模块的启动!!! -->
|
||||
<!-- <module>yudao-module-ai</module>-->
|
||||
</modules>
|
||||
@ -35,17 +36,17 @@
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<properties>
|
||||
<revision>2026.04-SNAPSHOT</revision>
|
||||
<revision>2026.05-SNAPSHOT</revision>
|
||||
<!-- Maven 相关 -->
|
||||
<java.version>21</java.version>
|
||||
<java.version>17</java.version>
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||
<maven-surefire-plugin.version>3.5.3</maven-surefire-plugin.version>
|
||||
<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.46</lombok.version>
|
||||
<spring.boot.version>4.0.6</spring.boot.version>
|
||||
<lombok.version>1.18.42</lombok.version>
|
||||
<spring.boot.version>3.5.9</spring.boot.version>
|
||||
<mapstruct.version>1.6.3</mapstruct.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
@ -149,14 +150,6 @@
|
||||
|
||||
<!-- 使用 huawei / aliyun 的 Maven 源,提升下载速度 -->
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>huaweicloud</id>
|
||||
<name>huawei</name>
|
||||
|
||||
32
script/livekit-poc/README.md
Normal file
32
script/livekit-poc/README.md
Normal file
@ -0,0 +1,32 @@
|
||||
# LiveKit Server PoC
|
||||
|
||||
最小可用的 LiveKit Server 自部署验证环境,用于零期 PoC。
|
||||
|
||||
## 启动
|
||||
|
||||
```bash
|
||||
cd tools/livekit-poc
|
||||
docker compose up -d
|
||||
bash verify.sh
|
||||
```
|
||||
|
||||
## 端口
|
||||
|
||||
- 7880:HTTP / WebSocket 信令;
|
||||
- 7881:WebRTC TCP fallback;
|
||||
- 7882/UDP:WebRTC 媒体;
|
||||
- macOS / Windows:当前 `docker-compose.yml` 走端口映射模式,webhook URL 用 `host.docker.internal:48080` 让容器访问到宿主机 yudao 后端;
|
||||
- macOS 上 host network(`network_mode: host`)需要 Docker Desktop 4.34+ 并在 Settings → Resources → Network 勾选「Enable host networking」,老版本静默失败(容器跑得起来但端口完全不通);
|
||||
- Linux:可以把 `docker-compose.yml` 改成 `network_mode: host` + 删 `ports:` 段,并把 `livekit.yaml` 的 webhook URL 改为 `http://127.0.0.1:48080/admin-api/im/livekit/webhook`。
|
||||
|
||||
## 凭据 (仅 PoC,勿用于生产)
|
||||
|
||||
- `LIVEKIT_KEYS=devkey: secret-poc-key-min-32-chars-required-here`
|
||||
- API Key:`devkey`
|
||||
- API Secret:`secret-poc-key-min-32-chars-required-here`
|
||||
|
||||
生产环境必须改用强随机 secret,并通过 `--config /etc/livekit.yaml` 加载。
|
||||
|
||||
## 浏览器联调
|
||||
|
||||
`verify.sh` 跑完会输出一个 `meet.livekit.io` 链接,用两个浏览器(或两台机器)打开同一链接即可看到对方画面。
|
||||
20
script/livekit-poc/docker-compose.yml
Normal file
20
script/livekit-poc/docker-compose.yml
Normal file
@ -0,0 +1,20 @@
|
||||
services:
|
||||
livekit:
|
||||
image: docker.m.daocloud.io/livekit/livekit-server:latest
|
||||
container_name: yudao-livekit-dev
|
||||
restart: unless-stopped
|
||||
# 端口映射模式
|
||||
# macOS / Windows 必走这种方式:Docker Desktop 4.34 以下没有 host network
|
||||
# Linux 可以改 network_mode: host 省去映射,并把 livekit.yaml 的 webhook url 换成 127.0.0.1
|
||||
ports:
|
||||
- "7880:7880" # HTTP / WebSocket 信令
|
||||
- "7881:7881" # WebRTC TCP fallback
|
||||
- "7882:7882/udp" # WebRTC UDP (dev 模式 UDP mux 单端口)
|
||||
volumes:
|
||||
# 挂载 config 文件;webhook 配置在 livekit.yaml 里
|
||||
- ./livekit.yaml:/etc/livekit.yaml:ro
|
||||
command:
|
||||
- --config
|
||||
- /etc/livekit.yaml
|
||||
- --bind
|
||||
- 0.0.0.0
|
||||
30
script/livekit-poc/livekit.yaml
Normal file
30
script/livekit-poc/livekit.yaml
Normal file
@ -0,0 +1,30 @@
|
||||
# LiveKit Server 本地开发配置(PoC 用,勿用于生产)
|
||||
# 替代 docker --dev 模式;为支持 webhook 必须用 config 文件而非 env
|
||||
|
||||
keys:
|
||||
devkey: secret-poc-key-min-32-chars-required-here
|
||||
|
||||
# 端口
|
||||
port: 7880
|
||||
rtc:
|
||||
tcp_port: 7881
|
||||
udp_port: 7882
|
||||
use_external_ip: false
|
||||
|
||||
# Webhook:成员离开 / 房间结束等事件回调到 yudao 后端做业务态兜底清理
|
||||
# host.docker.internal 让容器访问宿主机 macOS / Windows 上的 yudao 后端
|
||||
# Linux 上 docker compose 可改 network_mode: host,这里同步改成 127.0.0.1
|
||||
# api_key 用于签发 JWT,yudao 后端用相同 secret 验证签名
|
||||
webhook:
|
||||
api_key: devkey
|
||||
urls:
|
||||
- http://host.docker.internal:48080/admin-api/im/livekit/webhook
|
||||
|
||||
# 房间无人时多久销毁;秒;之前 PoC 默认 300,给低些方便排查
|
||||
room:
|
||||
empty_timeout: 300
|
||||
departure_timeout: 20
|
||||
|
||||
# 开发模式:放宽 secret 长度限制 + 内置 TURN 服务
|
||||
development: true
|
||||
log_level: info
|
||||
105
script/livekit-poc/verify.sh
Executable file
105
script/livekit-poc/verify.sh
Executable file
@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env bash
|
||||
# LiveKit Server PoC 验证脚本;
|
||||
# 用法: bash verify.sh
|
||||
set -e
|
||||
|
||||
API_KEY="${LIVEKIT_API_KEY:-devkey}"
|
||||
API_SECRET="${LIVEKIT_API_SECRET:-secret-poc-key-min-32-chars-required-here}"
|
||||
HOST="${LIVEKIT_HOST:-localhost:7880}"
|
||||
ROOM="${LIVEKIT_ROOM:-poc-room}"
|
||||
|
||||
ok() { printf "[OK] %s\n" "$1"; }
|
||||
fail() { printf "[FAIL] %s\n" "$1"; exit 1; }
|
||||
|
||||
echo "==> 1/5 等待 HTTP 端点就绪 (http://${HOST}/)"
|
||||
for i in $(seq 1 20); do
|
||||
code=$(curl -s -o /dev/null -w "%{http_code}" "http://${HOST}/" || echo "000")
|
||||
[ "$code" = "200" ] && { ok "HTTP 200"; break; }
|
||||
[ $i -eq 20 ] && fail "20 秒内未就绪 (last code=${code})"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "==> 2/5 签发管理 + 客户端权限 Token"
|
||||
TOKEN=$(API_KEY="$API_KEY" API_SECRET="$API_SECRET" ROOM="$ROOM" python3 - <<'PY'
|
||||
import json, time, hmac, hashlib, base64, os
|
||||
def b64u(b): return base64.urlsafe_b64encode(b).rstrip(b'=').decode()
|
||||
header = b64u(json.dumps({"alg":"HS256","typ":"JWT"}, separators=(',',':')).encode())
|
||||
payload = b64u(json.dumps({
|
||||
"iss": os.environ["API_KEY"],
|
||||
"sub": "poc-tester",
|
||||
"name": "PoC Tester",
|
||||
"video": {
|
||||
"roomJoin": True, "room": os.environ["ROOM"],
|
||||
"canPublish": True, "canSubscribe": True, "canPublishData": True,
|
||||
"roomCreate": True, "roomList": True, "roomAdmin": True
|
||||
},
|
||||
"exp": int(time.time()) + 3600,
|
||||
"nbf": int(time.time())
|
||||
}, separators=(',',':')).encode())
|
||||
sig = b64u(hmac.new(os.environ["API_SECRET"].encode(),
|
||||
f"{header}.{payload}".encode(),
|
||||
hashlib.sha256).digest())
|
||||
print(f"{header}.{payload}.{sig}")
|
||||
PY
|
||||
)
|
||||
[ -n "$TOKEN" ] || fail "Token 生成失败"
|
||||
ok "Token 已生成 (${#TOKEN} chars)"
|
||||
|
||||
echo "==> 3/5 创建房间 ${ROOM} (CreateRoom RPC)"
|
||||
create_resp=$(curl -s -X POST "http://${HOST}/twirp/livekit.RoomService/CreateRoom" \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"${ROOM}\",\"empty_timeout\":300,\"max_participants\":10}")
|
||||
echo " 响应: $create_resp"
|
||||
echo "$create_resp" | jq -e '.sid' >/dev/null 2>&1 \
|
||||
&& ok "房间已创建" \
|
||||
|| fail "CreateRoom 失败"
|
||||
|
||||
echo "==> 4/5 列出房间 (ListRooms RPC)"
|
||||
list_resp=$(curl -s -X POST "http://${HOST}/twirp/livekit.RoomService/ListRooms" \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}')
|
||||
room_count=$(echo "$list_resp" | jq '.rooms | length' 2>/dev/null || echo "0")
|
||||
ok "当前房间数: ${room_count}"
|
||||
echo "$list_resp" | jq '.'
|
||||
|
||||
echo "==> 5/5 删除房间 (DeleteRoom RPC) —— 清理"
|
||||
del_resp=$(curl -s -X POST "http://${HOST}/twirp/livekit.RoomService/DeleteRoom" \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"room\":\"${ROOM}\"}")
|
||||
ok "删除响应: $del_resp"
|
||||
|
||||
# 重新签一个仅 client 权限的 token;用于浏览器进会
|
||||
CLIENT_TOKEN=$(API_KEY="$API_KEY" API_SECRET="$API_SECRET" ROOM="$ROOM" python3 - <<'PY'
|
||||
import json, time, hmac, hashlib, base64, os
|
||||
def b64u(b): return base64.urlsafe_b64encode(b).rstrip(b'=').decode()
|
||||
header = b64u(json.dumps({"alg":"HS256","typ":"JWT"}, separators=(',',':')).encode())
|
||||
payload = b64u(json.dumps({
|
||||
"iss": os.environ["API_KEY"],
|
||||
"sub": "browser-tester",
|
||||
"name": "Browser",
|
||||
"video": {
|
||||
"roomJoin": True, "room": os.environ["ROOM"],
|
||||
"canPublish": True, "canSubscribe": True, "canPublishData": True
|
||||
},
|
||||
"exp": int(time.time()) + 7200
|
||||
}, separators=(',',':')).encode())
|
||||
sig = b64u(hmac.new(os.environ["API_SECRET"].encode(),
|
||||
f"{header}.{payload}".encode(),
|
||||
hashlib.sha256).digest())
|
||||
print(f"{header}.{payload}.{sig}")
|
||||
PY
|
||||
)
|
||||
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo " LiveKit Server 验证通过"
|
||||
echo "============================================================"
|
||||
echo " 浏览器测试 (开两个窗口能互通):"
|
||||
echo " https://meet.livekit.io/?liveKitUrl=ws%3A%2F%2F${HOST}&token=${CLIENT_TOKEN}"
|
||||
echo ""
|
||||
echo " 停止服务:"
|
||||
echo " docker compose -f tools/livekit-poc/docker-compose.yml down"
|
||||
echo "============================================================"
|
||||
@ -1552,9 +1552,9 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3443, 1, '草稿', '0', 'mes_wm_product_produce_status', 0, 'info', '', '草稿状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3444, 2, '已完成', '4', 'mes_wm_product_produce_status', 0, 'success', '', '已完成状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3445, 3, '已取消', '5', 'mes_wm_product_produce_status', 0, 'danger', '', '已取消状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3446, 0, 'è‰ç¨¿', '0', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3447, 1, '已完æˆ', '4', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3448, 2, '已喿¶ˆ', '5', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3446, 0, '草稿', '0', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3447, 1, '已完成', '4', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3448, 2, '已取消', '5', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||
COMMIT;
|
||||
SET IDENTITY_INSERT system_dict_data OFF;
|
||||
-- @formatter:on
|
||||
@ -5617,4 +5617,3 @@ INSERT INTO yudao_demo03_student (id, name, sex, birthday, description, creator,
|
||||
COMMIT;
|
||||
SET IDENTITY_INSERT yudao_demo03_student OFF;
|
||||
-- @formatter:on
|
||||
|
||||
|
||||
208
sql/highgo/quartz.sql
Normal file
208
sql/highgo/quartz.sql
Normal file
@ -0,0 +1,208 @@
|
||||
-- https://github.com/quartz-scheduler/quartz/blob/main/quartz/src/main/resources/org/quartz/impl/jdbcjobstore/tables_postgres.sql
|
||||
-- Thanks to Patrick Lightbody for submitting this...
|
||||
--
|
||||
-- In your Quartz properties file, you'll need to set
|
||||
-- org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
|
||||
|
||||
DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
|
||||
DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
|
||||
DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
|
||||
DROP TABLE IF EXISTS QRTZ_LOCKS;
|
||||
DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
|
||||
DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
|
||||
DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS;
|
||||
DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
|
||||
DROP TABLE IF EXISTS QRTZ_TRIGGERS;
|
||||
DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
|
||||
DROP TABLE IF EXISTS QRTZ_CALENDARS;
|
||||
|
||||
CREATE TABLE QRTZ_JOB_DETAILS
|
||||
(
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
JOB_NAME VARCHAR(200) NOT NULL,
|
||||
JOB_GROUP VARCHAR(200) NOT NULL,
|
||||
DESCRIPTION VARCHAR(250) NULL,
|
||||
JOB_CLASS_NAME VARCHAR(250) NOT NULL,
|
||||
IS_DURABLE BOOL NOT NULL,
|
||||
IS_NONCONCURRENT BOOL NOT NULL,
|
||||
IS_UPDATE_DATA BOOL NOT NULL,
|
||||
REQUESTS_RECOVERY BOOL NOT NULL,
|
||||
JOB_DATA BYTEA NULL,
|
||||
PRIMARY KEY (SCHED_NAME, JOB_NAME, JOB_GROUP)
|
||||
);
|
||||
|
||||
CREATE TABLE QRTZ_TRIGGERS
|
||||
(
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
TRIGGER_NAME VARCHAR(200) NOT NULL,
|
||||
TRIGGER_GROUP VARCHAR(200) NOT NULL,
|
||||
JOB_NAME VARCHAR(200) NOT NULL,
|
||||
JOB_GROUP VARCHAR(200) NOT NULL,
|
||||
DESCRIPTION VARCHAR(250) NULL,
|
||||
NEXT_FIRE_TIME BIGINT NULL,
|
||||
PREV_FIRE_TIME BIGINT NULL,
|
||||
PRIORITY INTEGER NULL,
|
||||
TRIGGER_STATE VARCHAR(16) NOT NULL,
|
||||
TRIGGER_TYPE VARCHAR(8) NOT NULL,
|
||||
START_TIME BIGINT NOT NULL,
|
||||
END_TIME BIGINT NULL,
|
||||
CALENDAR_NAME VARCHAR(200) NULL,
|
||||
MISFIRE_INSTR SMALLINT NULL,
|
||||
JOB_DATA BYTEA NULL,
|
||||
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
|
||||
FOREIGN KEY (SCHED_NAME, JOB_NAME, JOB_GROUP)
|
||||
REFERENCES QRTZ_JOB_DETAILS (SCHED_NAME, JOB_NAME, JOB_GROUP)
|
||||
);
|
||||
|
||||
CREATE TABLE QRTZ_SIMPLE_TRIGGERS
|
||||
(
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
TRIGGER_NAME VARCHAR(200) NOT NULL,
|
||||
TRIGGER_GROUP VARCHAR(200) NOT NULL,
|
||||
REPEAT_COUNT BIGINT NOT NULL,
|
||||
REPEAT_INTERVAL BIGINT NOT NULL,
|
||||
TIMES_TRIGGERED BIGINT NOT NULL,
|
||||
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
|
||||
FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
|
||||
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
|
||||
);
|
||||
|
||||
CREATE TABLE QRTZ_CRON_TRIGGERS
|
||||
(
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
TRIGGER_NAME VARCHAR(200) NOT NULL,
|
||||
TRIGGER_GROUP VARCHAR(200) NOT NULL,
|
||||
CRON_EXPRESSION VARCHAR(120) NOT NULL,
|
||||
TIME_ZONE_ID VARCHAR(80),
|
||||
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
|
||||
FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
|
||||
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
|
||||
);
|
||||
|
||||
CREATE TABLE QRTZ_SIMPROP_TRIGGERS
|
||||
(
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
TRIGGER_NAME VARCHAR(200) NOT NULL,
|
||||
TRIGGER_GROUP VARCHAR(200) NOT NULL,
|
||||
STR_PROP_1 VARCHAR(512) NULL,
|
||||
STR_PROP_2 VARCHAR(512) NULL,
|
||||
STR_PROP_3 VARCHAR(512) NULL,
|
||||
INT_PROP_1 INT NULL,
|
||||
INT_PROP_2 INT NULL,
|
||||
LONG_PROP_1 BIGINT NULL,
|
||||
LONG_PROP_2 BIGINT NULL,
|
||||
DEC_PROP_1 NUMERIC(13, 4) NULL,
|
||||
DEC_PROP_2 NUMERIC(13, 4) NULL,
|
||||
BOOL_PROP_1 BOOL NULL,
|
||||
BOOL_PROP_2 BOOL NULL,
|
||||
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
|
||||
FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
|
||||
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
|
||||
);
|
||||
|
||||
CREATE TABLE QRTZ_BLOB_TRIGGERS
|
||||
(
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
TRIGGER_NAME VARCHAR(200) NOT NULL,
|
||||
TRIGGER_GROUP VARCHAR(200) NOT NULL,
|
||||
BLOB_DATA BYTEA NULL,
|
||||
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
|
||||
FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
|
||||
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
|
||||
);
|
||||
|
||||
CREATE TABLE QRTZ_CALENDARS
|
||||
(
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
CALENDAR_NAME VARCHAR(200) NOT NULL,
|
||||
CALENDAR BYTEA NOT NULL,
|
||||
PRIMARY KEY (SCHED_NAME, CALENDAR_NAME)
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS
|
||||
(
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
TRIGGER_GROUP VARCHAR(200) NOT NULL,
|
||||
PRIMARY KEY (SCHED_NAME, TRIGGER_GROUP)
|
||||
);
|
||||
|
||||
CREATE TABLE QRTZ_FIRED_TRIGGERS
|
||||
(
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
ENTRY_ID VARCHAR(95) NOT NULL,
|
||||
TRIGGER_NAME VARCHAR(200) NOT NULL,
|
||||
TRIGGER_GROUP VARCHAR(200) NOT NULL,
|
||||
INSTANCE_NAME VARCHAR(200) NOT NULL,
|
||||
FIRED_TIME BIGINT NOT NULL,
|
||||
SCHED_TIME BIGINT NOT NULL,
|
||||
PRIORITY INTEGER NOT NULL,
|
||||
STATE VARCHAR(16) NOT NULL,
|
||||
JOB_NAME VARCHAR(200) NULL,
|
||||
JOB_GROUP VARCHAR(200) NULL,
|
||||
IS_NONCONCURRENT BOOL NULL,
|
||||
REQUESTS_RECOVERY BOOL NULL,
|
||||
PRIMARY KEY (SCHED_NAME, ENTRY_ID)
|
||||
);
|
||||
|
||||
CREATE TABLE QRTZ_SCHEDULER_STATE
|
||||
(
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
INSTANCE_NAME VARCHAR(200) NOT NULL,
|
||||
LAST_CHECKIN_TIME BIGINT NOT NULL,
|
||||
CHECKIN_INTERVAL BIGINT NOT NULL,
|
||||
PRIMARY KEY (SCHED_NAME, INSTANCE_NAME)
|
||||
);
|
||||
|
||||
CREATE TABLE QRTZ_LOCKS
|
||||
(
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
LOCK_NAME VARCHAR(40) NOT NULL,
|
||||
PRIMARY KEY (SCHED_NAME, LOCK_NAME)
|
||||
);
|
||||
|
||||
CREATE INDEX IDX_QRTZ_J_REQ_RECOVERY
|
||||
ON QRTZ_JOB_DETAILS (SCHED_NAME, REQUESTS_RECOVERY);
|
||||
CREATE INDEX IDX_QRTZ_J_GRP
|
||||
ON QRTZ_JOB_DETAILS (SCHED_NAME, JOB_GROUP);
|
||||
|
||||
CREATE INDEX IDX_QRTZ_T_J
|
||||
ON QRTZ_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP);
|
||||
CREATE INDEX IDX_QRTZ_T_JG
|
||||
ON QRTZ_TRIGGERS (SCHED_NAME, JOB_GROUP);
|
||||
CREATE INDEX IDX_QRTZ_T_C
|
||||
ON QRTZ_TRIGGERS (SCHED_NAME, CALENDAR_NAME);
|
||||
CREATE INDEX IDX_QRTZ_T_G
|
||||
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_GROUP);
|
||||
CREATE INDEX IDX_QRTZ_T_STATE
|
||||
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE);
|
||||
CREATE INDEX IDX_QRTZ_T_N_STATE
|
||||
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP, TRIGGER_STATE);
|
||||
CREATE INDEX IDX_QRTZ_T_N_G_STATE
|
||||
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_GROUP, TRIGGER_STATE);
|
||||
CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME
|
||||
ON QRTZ_TRIGGERS (SCHED_NAME, NEXT_FIRE_TIME);
|
||||
CREATE INDEX IDX_QRTZ_T_NFT_ST
|
||||
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE, NEXT_FIRE_TIME);
|
||||
CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE
|
||||
ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME);
|
||||
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE
|
||||
ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_STATE);
|
||||
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP
|
||||
ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_GROUP, TRIGGER_STATE);
|
||||
|
||||
CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME
|
||||
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME);
|
||||
CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY
|
||||
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME, REQUESTS_RECOVERY);
|
||||
CREATE INDEX IDX_QRTZ_FT_J_G
|
||||
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP);
|
||||
CREATE INDEX IDX_QRTZ_FT_JG
|
||||
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, JOB_GROUP);
|
||||
CREATE INDEX IDX_QRTZ_FT_T_G
|
||||
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP);
|
||||
CREATE INDEX IDX_QRTZ_FT_TG
|
||||
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, TRIGGER_GROUP);
|
||||
|
||||
|
||||
COMMIT;
|
||||
6198
sql/highgo/ruoyi-vue-pro.sql
Normal file
6198
sql/highgo/ruoyi-vue-pro.sql
Normal file
File diff suppressed because it is too large
Load Diff
@ -1653,9 +1653,9 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3443, 1, '草稿', '0', 'mes_wm_product_produce_status', 0, 'info', '', '草稿状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3444, 2, '已完成', '4', 'mes_wm_product_produce_status', 0, 'success', '', '已完成状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3445, 3, '已取消', '5', 'mes_wm_product_produce_status', 0, 'danger', '', '已取消状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3446, 0, 'è‰ç¨¿', '0', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3447, 1, '已完æˆ', '4', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3448, 2, '已喿¶ˆ', '5', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3446, 0, '草稿', '0', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3447, 1, '已完成', '4', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3448, 2, '已取消', '5', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||
COMMIT;
|
||||
-- @formatter:on
|
||||
|
||||
@ -5943,4 +5943,3 @@ COMMIT;
|
||||
DROP SEQUENCE IF EXISTS yudao_demo03_student_seq;
|
||||
CREATE SEQUENCE yudao_demo03_student_seq
|
||||
START 10;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1653,9 +1653,9 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3443, 1, '草稿', '0', 'mes_wm_product_produce_status', 0, 'info', '', '草稿状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3444, 2, '已完成', '4', 'mes_wm_product_produce_status', 0, 'success', '', '已完成状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3445, 3, '已取消', '5', 'mes_wm_product_produce_status', 0, 'danger', '', '已取消状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3446, 0, 'è‰ç¨¿', '0', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3447, 1, '已完æˆ', '4', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3448, 2, '已喿¶ˆ', '5', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3446, 0, '草稿', '0', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3447, 1, '已完成', '4', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3448, 2, '已取消', '5', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||
COMMIT;
|
||||
-- @formatter:on
|
||||
|
||||
@ -5943,4 +5943,3 @@ COMMIT;
|
||||
DROP SEQUENCE IF EXISTS yudao_demo03_student_seq;
|
||||
CREATE SEQUENCE yudao_demo03_student_seq
|
||||
START 10;
|
||||
|
||||
|
||||
@ -1605,9 +1605,9 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3443, 1, '草稿', '0', 'mes_wm_product_produce_status', 0, 'info', '', '草稿状态', '1', to_date('2026-04-05 15:53:46', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-04-05 15:53:46', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3444, 2, '已完成', '4', 'mes_wm_product_produce_status', 0, 'success', '', '已完成状态', '1', to_date('2026-04-05 15:53:46', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-04-05 15:53:46', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3445, 3, '已取消', '5', 'mes_wm_product_produce_status', 0, 'danger', '', '已取消状态', '1', to_date('2026-04-05 15:53:46', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-04-05 15:53:46', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3446, 0, 'è‰ç¨¿', '0', 'mes_pro_task_status', 0, '', '', NULL, '1', to_date('2026-04-16 09:47:00', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-04-16 09:47:00', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3447, 1, '已完æˆ', '4', 'mes_pro_task_status', 0, '', '', NULL, '1', to_date('2026-04-16 09:47:00', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-04-16 09:47:00', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3448, 2, '已喿¶ˆ', '5', 'mes_pro_task_status', 0, '', '', NULL, '1', to_date('2026-04-16 09:47:00', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-04-16 09:47:00', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3446, 0, '草稿', '0', 'mes_pro_task_status', 0, '', '', NULL, '1', to_date('2026-04-16 09:47:00', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-04-16 09:47:00', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3447, 1, '已完成', '4', 'mes_pro_task_status', 0, '', '', NULL, '1', to_date('2026-04-16 09:47:00', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-04-16 09:47:00', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3448, 2, '已取消', '5', 'mes_pro_task_status', 0, '', '', NULL, '1', to_date('2026-04-16 09:47:00', 'SYYYY-MM-DD HH24:MI:SS'), '1', to_date('2026-04-16 09:47:00', 'SYYYY-MM-DD HH24:MI:SS'), '0');
|
||||
COMMIT;
|
||||
-- @formatter:on
|
||||
|
||||
@ -5801,4 +5801,3 @@ COMMIT;
|
||||
|
||||
CREATE SEQUENCE yudao_demo03_student_seq
|
||||
START WITH 10;
|
||||
|
||||
|
||||
@ -1653,9 +1653,9 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3443, 1, '草稿', '0', 'mes_wm_product_produce_status', 0, 'info', '', '草稿状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3444, 2, '已完成', '4', 'mes_wm_product_produce_status', 0, 'success', '', '已完成状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3445, 3, '已取消', '5', 'mes_wm_product_produce_status', 0, 'danger', '', '已取消状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3446, 0, 'è‰ç¨¿', '0', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3447, 1, '已完æˆ', '4', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3448, 2, '已喿¶ˆ', '5', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3446, 0, '草稿', '0', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3447, 1, '已完成', '4', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3448, 2, '已取消', '5', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', '0');
|
||||
COMMIT;
|
||||
-- @formatter:on
|
||||
|
||||
@ -5943,4 +5943,3 @@ COMMIT;
|
||||
DROP SEQUENCE IF EXISTS yudao_demo03_student_seq;
|
||||
CREATE SEQUENCE yudao_demo03_student_seq
|
||||
START 10;
|
||||
|
||||
|
||||
@ -3931,11 +3931,11 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
|
||||
GO
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3445, 3, N'已取消', N'5', N'mes_wm_product_produce_status', 0, N'danger', N'', N'已取消状态', N'1', N'2026-04-05 15:53:46', N'1', N'2026-04-05 15:53:46', N'0')
|
||||
GO
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3446, 0, N'è‰ç¨¿', N'0', N'mes_pro_task_status', 0, N'', N'', NULL, N'1', N'2026-04-16 09:47:00', N'1', N'2026-04-16 09:47:00', N'0')
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3446, 0, N'草稿', N'0', N'mes_pro_task_status', 0, N'', N'', NULL, N'1', N'2026-04-16 09:47:00', N'1', N'2026-04-16 09:47:00', N'0')
|
||||
GO
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3447, 1, N'已完æˆ', N'4', N'mes_pro_task_status', 0, N'', N'', NULL, N'1', N'2026-04-16 09:47:00', N'1', N'2026-04-16 09:47:00', N'0')
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3447, 1, N'已完成', N'4', N'mes_pro_task_status', 0, N'', N'', NULL, N'1', N'2026-04-16 09:47:00', N'1', N'2026-04-16 09:47:00', N'0')
|
||||
GO
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3448, 2, N'已喿¶ˆ', N'5', N'mes_pro_task_status', 0, N'', N'', NULL, N'1', N'2026-04-16 09:47:00', N'1', N'2026-04-16 09:47:00', N'0')
|
||||
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3448, 2, N'已取消', N'5', N'mes_pro_task_status', 0, N'', N'', NULL, N'1', N'2026-04-16 09:47:00', N'1', N'2026-04-16 09:47:00', N'0')
|
||||
GO
|
||||
SET IDENTITY_INSERT system_dict_data OFF
|
||||
GO
|
||||
@ -13915,4 +13915,3 @@ GO
|
||||
COMMIT
|
||||
GO
|
||||
-- @formatter:on
|
||||
|
||||
|
||||
@ -90,6 +90,25 @@ docker compose up -d opengauss
|
||||
docker compose exec opengauss bash -c '/usr/local/opengauss/bin/gsql -U $GS_USERNAME -W $GS_PASSWORD -d postgres -f /tmp/schema.sql'
|
||||
```
|
||||
|
||||
### 1.8 HighGo 瀚高数据库
|
||||
|
||||
① 下载瀚高官方 Docker 镜像,并加载镜像文件。加载后,将镜像打成本地标签:
|
||||
|
||||
```Bash
|
||||
docker load -i <highgo-image>.tar
|
||||
docker tag <image>:<tag> highgo:local
|
||||
```
|
||||
|
||||
② 在项目 `sql/tools` 目录下运行:
|
||||
|
||||
```Bash
|
||||
docker compose up -d highgo
|
||||
```
|
||||
|
||||
> 注意:不同瀚高镜像的数据目录可能不同,如果容器无法启动,请按镜像实际 `PGDATA` 修改 `docker-compose.yaml` 中的 `highgo` 数据卷挂载目录。
|
||||
|
||||
③ 启动完成后,需要手动导入 Quartz 和项目 SQL。瀚高兼容 PostgreSQL,具体客户端命令以当前镜像为准,可使用 `psql` 或瀚高镜像内置的兼容客户端执行 `/tmp/quartz.sql`、`/tmp/schema.sql`。
|
||||
|
||||
## 1.X 容器的销毁重建
|
||||
|
||||
开发测试过程中,有时候需要创建全新干净的数据库。由于测试数据 Docker 容器采用数据卷 Volume 挂载数据库实例的数据目录,因此销毁数据需要停止容器后,删除数据卷,然后再重新创建容器。
|
||||
@ -103,7 +122,7 @@ docker volume rm ruoyi-vue-pro_postgres
|
||||
|
||||
## 2. MySQL 转换其它数据库
|
||||
|
||||
项目提供了 `sql/tools/convertor.py` 脚本,支持将 MySQL 转换为 Oracle、PostgreSQL、SQL Server、达梦、人大金仓、OpenGauss 等数据库的脚本。
|
||||
项目提供了 `sql/tools/convertor.py` 脚本,支持将 MySQL 转换为 Oracle、PostgreSQL、SQL Server、达梦、人大金仓、OpenGauss、瀚高等数据库的脚本。
|
||||
|
||||
### 2.1 实现原理
|
||||
|
||||
@ -118,11 +137,12 @@ pip install simple-ddl-parser
|
||||
# pip3 install simple-ddl-parser
|
||||
```
|
||||
|
||||
② 在 `sql/tools/` 目录下,执行如下命令打印生成 postgres 的脚本内容,其他可选参数有:`oracle`、`sqlserver`、`dm8`、`kingbase`、`opengauss`:
|
||||
② 在 `sql/tools/` 目录下,执行如下命令打印生成 postgres 的脚本内容,其他可选参数有:`oracle`、`sqlserver`、`dm8`、`kingbase`、`opengauss`、`highgo`:
|
||||
|
||||
```Bash
|
||||
python3 convertor.py postgres
|
||||
# python3 convertor.py postgres > tmp.sql
|
||||
# python3 convertor.py highgo ../mysql/ruoyi-vue-pro.sql > ../highgo/ruoyi-vue-pro.sql
|
||||
```
|
||||
|
||||
程序将 SQL 脚本打印到终端,可以重定向到临时文件 `tmp.sql`。
|
||||
|
||||
@ -10,6 +10,7 @@ uv run --with simple-ddl-parser convertor.py postgres ../mysql/ruoyi-vue-pro.sql
|
||||
uv run --with simple-ddl-parser convertor.py sqlserver ../mysql/ruoyi-vue-pro.sql > ../sqlserver/ruoyi-vue-pro.sql
|
||||
uv run --with simple-ddl-parser convertor.py kingbase ../mysql/ruoyi-vue-pro.sql > ../kingbase/ruoyi-vue-pro.sql
|
||||
uv run --with simple-ddl-parser convertor.py opengauss ../mysql/ruoyi-vue-pro.sql > ../opengauss/ruoyi-vue-pro.sql
|
||||
uv run --with simple-ddl-parser convertor.py highgo ../mysql/ruoyi-vue-pro.sql > ../highgo/ruoyi-vue-pro.sql
|
||||
uv run --with simple-ddl-parser convertor.py oracle ../mysql/ruoyi-vue-pro.sql > ../oracle/ruoyi-vue-pro.sql
|
||||
uv run --with simple-ddl-parser convertor.py dm8 ../mysql/ruoyi-vue-pro.sql > ../dm/ruoyi-vue-pro-dm8.sql
|
||||
"""
|
||||
@ -77,6 +78,9 @@ def load_and_clean(sql_file: str) -> str:
|
||||
|
||||
|
||||
class Convertor(ABC):
|
||||
# 不同数据库的关键字不完全一致;子类按需声明需要转义的列名。
|
||||
reserved_column_names = set()
|
||||
|
||||
def __init__(self, src: str, db_type) -> None:
|
||||
self.src = src
|
||||
self.db_type = db_type
|
||||
@ -179,6 +183,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}`"
|
||||
@ -204,18 +233,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"]:
|
||||
@ -223,7 +289,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
|
||||
@ -381,7 +449,7 @@ class PostgreSQLConvertor(Convertor):
|
||||
)
|
||||
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"]]
|
||||
@ -406,7 +474,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"
|
||||
)
|
||||
|
||||
@ -435,6 +503,7 @@ 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 脚本
|
||||
@ -482,6 +551,8 @@ INSERT INTO dual VALUES (1);
|
||||
|
||||
|
||||
class OracleConvertor(Convertor):
|
||||
reserved_column_names = {"level", "size"}
|
||||
|
||||
def __init__(self, src):
|
||||
super().__init__(src, "Oracle")
|
||||
|
||||
@ -526,10 +597,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"]]
|
||||
@ -554,7 +623,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"
|
||||
)
|
||||
|
||||
@ -586,6 +655,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}')",
|
||||
@ -907,6 +977,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"
|
||||
@ -925,7 +997,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"]]
|
||||
@ -945,23 +1017,32 @@ CREATE TABLE {table_name} (
|
||||
|
||||
|
||||
class OpengaussConvertor(KingbaseConvertor):
|
||||
reserved_column_names = set()
|
||||
|
||||
def __init__(self, src):
|
||||
super().__init__(src)
|
||||
self.db_type = "OpenGauss"
|
||||
|
||||
|
||||
class HighGoConvertor(PostgreSQLConvertor):
|
||||
def __init__(self, src):
|
||||
super().__init__(src)
|
||||
self.db_type = "HighGo"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="芋道系统数据库转换工具")
|
||||
parser.add_argument(
|
||||
"type",
|
||||
type=str,
|
||||
help="目标数据库类型",
|
||||
choices=["postgres", "oracle", "sqlserver", "dm8", "kingbase", "opengauss"],
|
||||
choices=["postgres", "oracle", "sqlserver", "dm8", "kingbase", "opengauss", "highgo"],
|
||||
)
|
||||
parser.add_argument(
|
||||
"path",
|
||||
type=str,
|
||||
help="源数据库脚本路径",
|
||||
nargs="?",
|
||||
default="../mysql/ruoyi-vue-pro.sql"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
@ -980,6 +1061,8 @@ def main():
|
||||
convertor = KingbaseConvertor(sql_file)
|
||||
elif args.type == "opengauss":
|
||||
convertor = OpengaussConvertor(sql_file)
|
||||
elif args.type == "highgo":
|
||||
convertor = HighGoConvertor(sql_file)
|
||||
else:
|
||||
raise NotImplementedError(f"不支持目标数据库类型: {args.type}")
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ volumes:
|
||||
dm8: { }
|
||||
kingbase: { }
|
||||
opengauss: { }
|
||||
highgo: { }
|
||||
|
||||
services:
|
||||
mysql:
|
||||
@ -131,4 +132,16 @@ services:
|
||||
volumes:
|
||||
- opengauss:/var/lib/opengauss
|
||||
- ../opengauss/ruoyi-vue-pro.sql:/tmp/schema.sql:ro
|
||||
# docker compose exec opengauss bash -c '/usr/local/opengauss/bin/gsql -U $GS_USERNAME -W $GS_PASSWORD -d postgres -f /tmp/schema.sql'
|
||||
# docker compose exec opengauss bash -c '/usr/local/opengauss/bin/gsql -U $GS_USERNAME -W $GS_PASSWORD -d postgres -f /tmp/schema.sql'
|
||||
|
||||
highgo:
|
||||
# 使用瀚高官方提供的 Docker 镜像,加载后打成本地标签:
|
||||
# docker tag <image>:<tag> highgo:local
|
||||
image: highgo:local
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5866:5866"
|
||||
volumes:
|
||||
- highgo:/home/highgo/hgdb/data
|
||||
- ../highgo/quartz.sql:/tmp/quartz.sql:ro
|
||||
- ../highgo/ruoyi-vue-pro.sql:/tmp/schema.sql:ro
|
||||
|
||||
@ -14,12 +14,12 @@
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<properties>
|
||||
<revision>2026.04-SNAPSHOT</revision>
|
||||
<revision>2026.05-SNAPSHOT</revision>
|
||||
<flatten-maven-plugin.version>1.7.2</flatten-maven-plugin.version>
|
||||
<!-- 统一依赖管理 -->
|
||||
<spring.boot.version>4.0.6</spring.boot.version>
|
||||
<spring.boot.version>3.5.14</spring.boot.version>
|
||||
<!-- Web 相关 -->
|
||||
<springdoc.version>3.0.3</springdoc.version>
|
||||
<springdoc.version>2.8.17</springdoc.version>
|
||||
<knife4j.version>4.5.0</knife4j.version>
|
||||
<!-- DB 相关 -->
|
||||
<druid.version>1.2.28</druid.version>
|
||||
@ -27,8 +27,8 @@
|
||||
<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.1.5</easy-trans.version>
|
||||
<redisson.version>4.3.1</redisson.version>
|
||||
<easy-trans.version>3.0.6</easy-trans.version>
|
||||
<redisson.version>4.4.0</redisson.version>
|
||||
<dm8.jdbc.version>8.1.3.140</dm8.jdbc.version>
|
||||
<kingbase.jdbc.version>9.0.1.jre7</kingbase.jdbc.version>
|
||||
<opengauss.jdbc.version>7.0.0-RC3-og</opengauss.jdbc.version>
|
||||
@ -39,17 +39,19 @@
|
||||
<lock4j.version>2.2.7</lock4j.version>
|
||||
<!-- 监控相关 -->
|
||||
<skywalking.version>9.6.0</skywalking.version>
|
||||
<spring-boot-admin.version>4.0.4</spring-boot-admin.version>
|
||||
<spring-boot-admin.version>3.5.8</spring-boot-admin.version>
|
||||
<opentracing.version>0.33.0</opentracing.version>
|
||||
<!-- Test 测试相关 -->
|
||||
<podam.version>8.0.2.RELEASE</podam.version>
|
||||
<jedis-mock.version>1.1.12</jedis-mock.version>
|
||||
<mockito-inline.version>5.2.0</mockito-inline.version>
|
||||
<!-- Bpm 工作流相关 -->
|
||||
<flowable.version>7.2.0</flowable.version>
|
||||
<flowable.version>8.0.0</flowable.version>
|
||||
<!-- 工具类相关 -->
|
||||
<anji-plus-captcha.version>1.4.0</anji-plus-captcha.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.44</hutool-5.version>
|
||||
@ -57,6 +59,7 @@
|
||||
<fastexcel.version>1.3.0</fastexcel.version>
|
||||
<velocity.version>2.4.1</velocity.version>
|
||||
<fastjson.version>1.2.83</fastjson.version>
|
||||
<fastjson2.version>2.0.61</fastjson2.version>
|
||||
<guava.version>33.6.0-jre</guava.version>
|
||||
<transmittable-thread-local.version>2.14.5</transmittable-thread-local.version>
|
||||
<commons-net.version>3.13.0</commons-net.version>
|
||||
@ -65,7 +68,7 @@
|
||||
<tika-core.version>3.3.0</tika-core.version>
|
||||
<ip2region.version>2.7.0</ip2region.version>
|
||||
<bizlog-sdk.version>3.0.6</bizlog-sdk.version>
|
||||
<netty.version>4.2.12.Final</netty.version>
|
||||
<netty.version>4.2.14.Final</netty.version>
|
||||
<mqtt.version>1.2.5</mqtt.version>
|
||||
<vertx.version>4.5.26</vertx.version>
|
||||
<okhttp.version>4.12.0</okhttp.version>
|
||||
@ -75,10 +78,11 @@
|
||||
<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.3.2</jimureport.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>
|
||||
<alipay-sdk-java.version>4.40.771.ALL</alipay-sdk-java.version>
|
||||
<bouncycastle.version>1.80</bouncycastle.version>
|
||||
<alipay-sdk-java.version>4.40.806.ALL</alipay-sdk-java.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
@ -135,17 +139,6 @@
|
||||
<version>${spring.boot.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aspectj</artifactId>
|
||||
<version>${spring.boot.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-jackson</artifactId>
|
||||
<version>${spring.boot.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Web 相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
@ -191,7 +184,7 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>druid-spring-boot-4-starter</artifactId>
|
||||
<artifactId>druid-spring-boot-3-starter</artifactId>
|
||||
<version>${druid.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@ -202,7 +195,7 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot4-starter</artifactId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@ -217,7 +210,7 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>dynamic-datasource-spring-boot4-starter</artifactId> <!-- 多数据源 -->
|
||||
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId> <!-- 多数据源 -->
|
||||
<version>${dynamic-datasource.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@ -227,7 +220,7 @@
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId> <!-- VO 数据翻译 -->
|
||||
<groupId>com.fhs-opensource</groupId> <!-- VO 数据翻译 -->
|
||||
<artifactId>easy-trans-spring-boot-starter</artifactId>
|
||||
<version>${easy-trans.version}</version>
|
||||
<exclusions>
|
||||
@ -242,12 +235,12 @@
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<groupId>com.fhs-opensource</groupId>
|
||||
<artifactId>easy-trans-mybatis-plus-extend</artifactId>
|
||||
<version>${easy-trans.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<groupId>com.fhs-opensource</groupId>
|
||||
<artifactId>easy-trans-anno</artifactId>
|
||||
<version>${easy-trans.version}</version>
|
||||
</dependency>
|
||||
@ -521,6 +514,11 @@
|
||||
<artifactId>fastjson</artifactId>
|
||||
<version>${fastjson.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
<version>${fastjson2.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
@ -569,6 +567,18 @@
|
||||
<version>${jsoup.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.houbb</groupId>
|
||||
<artifactId>sensitive-word</artifactId> <!-- 敏感词检测:trie 树高效匹配 -->
|
||||
<version>${sensitive-word.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.belerweb</groupId>
|
||||
<artifactId>pinyin4j</artifactId> <!-- 汉字转拼音:作为 hutool PinyinUtil 的底层引擎 -->
|
||||
<version>${pinyin4j.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Vert.x -->
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
@ -657,6 +667,24 @@
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- 锁定 weixin-java 传递依赖,避免 Maven 版本范围自动升级到 1.80.2 后 Fat Jar 启动失败。
|
||||
反馈:https://t.zsxq.com/pCVBo -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk18on</artifactId>
|
||||
<version>${bouncycastle.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcutil-jdk18on</artifactId>
|
||||
<version>${bouncycastle.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcpkix-jdk18on</artifactId>
|
||||
<version>${bouncycastle.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>weixin-java-pay</artifactId>
|
||||
|
||||
@ -96,15 +96,20 @@
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>tools.jackson.core</groupId>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tools.jackson.core</groupId>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 -->
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
@ -129,7 +134,7 @@
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId> <!-- VO 数据翻译 -->
|
||||
<groupId>com.fhs-opensource</groupId> <!-- VO 数据翻译 -->
|
||||
<artifactId>easy-trans-anno</artifactId> <!-- 默认引入的原因,方便 xxx-module-api 包使用 -->
|
||||
</dependency>
|
||||
|
||||
|
||||
@ -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<>();
|
||||
@ -372,4 +388,14 @@ public class CollectionUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把单元素 head 与集合 tail 合并成新 List(head 在前,tail 顺序保留)
|
||||
*/
|
||||
public static <T> List<T> of(T head, Collection<T> tail) {
|
||||
List<T> list = new ArrayList<>();
|
||||
list.add(head);
|
||||
CollUtil.addAll(list, tail);
|
||||
return list;
|
||||
}
|
||||
|
||||
}
|
||||
@ -236,6 +236,23 @@ public class LocalDateTimeUtils {
|
||||
return LocalDateTime.now().with(TemporalAdjusters.firstDayOfYear()).with(LocalTime.MIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近 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) {
|
||||
|
||||
@ -9,6 +9,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import org.springframework.web.util.UriUtils;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URLDecoder;
|
||||
@ -55,11 +56,61 @@ public class HttpUtils {
|
||||
* @return 解码后的路径
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编码 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());
|
||||
// 先移除;再添加
|
||||
@ -200,4 +251,14 @@ public class HttpUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket URL 切换成 HTTP URL:ws:// → http://;wss:// → https://;其它格式原样保留
|
||||
*
|
||||
* @param url 原始 URL
|
||||
* @return 切换协议后的 URL
|
||||
*/
|
||||
public static String wsUrlToHttp(String url) {
|
||||
return StrUtil.startWithIgnoreCase(url, "ws") ? "http" + url.substring(2) : url;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -6,22 +6,23 @@ import cn.hutool.json.JSONUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.json.databind.TimestampLocalDateTimeDeserializer;
|
||||
import cn.iocoder.yudao.framework.common.util.json.databind.TimestampLocalDateTimeSerializer;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.DeserializationFeature;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.SerializationFeature;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
import tools.jackson.databind.module.SimpleModule;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* JSON 工具类
|
||||
@ -32,18 +33,17 @@ import java.util.List;
|
||||
public class JsonUtils {
|
||||
|
||||
@Getter
|
||||
private static ObjectMapper objectMapper = buildObjectMapper();
|
||||
private static ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
private static ObjectMapper buildObjectMapper() {
|
||||
SimpleModule simpleModule = new SimpleModule()
|
||||
static {
|
||||
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
|
||||
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 忽略 null 值
|
||||
// 解决 LocalDateTime 的序列化
|
||||
SimpleModule simpleModule = new JavaTimeModule()
|
||||
.addSerializer(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE)
|
||||
.addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE);
|
||||
return JsonMapper.builder()
|
||||
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
|
||||
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
|
||||
.changeDefaultPropertyInclusion(value -> JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL))
|
||||
.addModule(simpleModule)
|
||||
.build();
|
||||
objectMapper.registerModules(simpleModule);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -78,7 +78,7 @@ public class JsonUtils {
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(text, clazz);
|
||||
} catch (JacksonException e) {
|
||||
} catch (IOException e) {
|
||||
log.error("json parse err,json:{}", text, e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@ -92,7 +92,7 @@ public class JsonUtils {
|
||||
JsonNode treeNode = objectMapper.readTree(text);
|
||||
JsonNode pathNode = treeNode.path(path);
|
||||
return objectMapper.readValue(pathNode.toString(), clazz);
|
||||
} catch (JacksonException e) {
|
||||
} catch (IOException e) {
|
||||
log.error("json parse err,json:{}", text, e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@ -104,7 +104,7 @@ public class JsonUtils {
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(text, objectMapper.getTypeFactory().constructType(type));
|
||||
} catch (JacksonException e) {
|
||||
} catch (IOException e) {
|
||||
log.error("json parse err,json:{}", text, e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@ -116,7 +116,7 @@ public class JsonUtils {
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(text, objectMapper.getTypeFactory().constructType(type));
|
||||
} catch (JacksonException e) {
|
||||
} catch (IOException e) {
|
||||
log.error("json parse err,json:{}", text, e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@ -144,7 +144,7 @@ public class JsonUtils {
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(bytes, clazz);
|
||||
} catch (JacksonException e) {
|
||||
} catch (IOException e) {
|
||||
log.error("json parse err,json:{}", bytes, e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@ -153,7 +153,7 @@ public class JsonUtils {
|
||||
public static <T> T parseObject(String text, TypeReference<T> typeReference) {
|
||||
try {
|
||||
return objectMapper.readValue(text, typeReference);
|
||||
} catch (JacksonException e) {
|
||||
} catch (IOException e) {
|
||||
log.error("json parse err,json:{}", text, e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@ -169,7 +169,24 @@ public class JsonUtils {
|
||||
public static <T> T parseObjectQuietly(String text, TypeReference<T> typeReference) {
|
||||
try {
|
||||
return objectMapper.readValue(text, typeReference);
|
||||
} catch (JacksonException e) {
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 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;
|
||||
}
|
||||
}
|
||||
@ -187,7 +204,7 @@ public class JsonUtils {
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(text, clazz);
|
||||
} catch (JacksonException e) {
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -198,7 +215,7 @@ public class JsonUtils {
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
|
||||
} catch (JacksonException e) {
|
||||
} catch (IOException e) {
|
||||
log.error("json parse err,json:{}", text, e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@ -212,7 +229,7 @@ public class JsonUtils {
|
||||
JsonNode treeNode = objectMapper.readTree(text);
|
||||
JsonNode pathNode = treeNode.path(path);
|
||||
return objectMapper.readValue(pathNode.toString(), objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
|
||||
} catch (JacksonException e) {
|
||||
} catch (IOException e) {
|
||||
log.error("json parse err,json:{}", text, e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@ -221,7 +238,7 @@ public class JsonUtils {
|
||||
public static JsonNode parseTree(String text) {
|
||||
try {
|
||||
return objectMapper.readTree(text);
|
||||
} catch (JacksonException e) {
|
||||
} catch (IOException e) {
|
||||
log.error("json parse err,json:{}", text, e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@ -230,12 +247,20 @@ public class JsonUtils {
|
||||
public static JsonNode parseTree(byte[] text) {
|
||||
try {
|
||||
return objectMapper.readTree(text);
|
||||
} catch (JacksonException e) {
|
||||
} catch (IOException e) {
|
||||
log.error("json parse err,json:{}", text, e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
package cn.iocoder.yudao.framework.common.util.json.databind;
|
||||
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.core.JsonGenerator;
|
||||
import tools.jackson.databind.SerializationContext;
|
||||
import tools.jackson.databind.annotation.JacksonStdImpl;
|
||||
import tools.jackson.databind.ser.std.StdScalarSerializer;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Long 序列化规则
|
||||
@ -17,41 +14,24 @@ import java.math.BigInteger;
|
||||
* @author 星语
|
||||
*/
|
||||
@JacksonStdImpl
|
||||
public class NumberSerializer extends StdScalarSerializer<Number> {
|
||||
public class NumberSerializer extends com.fasterxml.jackson.databind.ser.std.NumberSerializer {
|
||||
|
||||
private static final long MAX_SAFE_INTEGER = 9007199254740991L;
|
||||
private static final long MIN_SAFE_INTEGER = -9007199254740991L;
|
||||
|
||||
public static final NumberSerializer INSTANCE = new NumberSerializer(Number.class);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public NumberSerializer(Class<? extends Number> rawType) {
|
||||
super((Class<Number>) rawType);
|
||||
super(rawType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(Number value, JsonGenerator gen, SerializationContext serializers) throws JacksonException {
|
||||
public void serialize(Number value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||
// 超出范围 序列化位字符串
|
||||
if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) {
|
||||
writeNumber(value, gen);
|
||||
super.serialize(value, gen, serializers);
|
||||
} else {
|
||||
gen.writeString(value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeNumber(Number value, JsonGenerator gen) throws JacksonException {
|
||||
if (value instanceof BigDecimal decimal) {
|
||||
gen.writeNumber(decimal);
|
||||
} else if (value instanceof BigInteger integer) {
|
||||
gen.writeNumber(integer);
|
||||
} else if (value instanceof Double doubleValue) {
|
||||
gen.writeNumber(doubleValue);
|
||||
} else if (value instanceof Float floatValue) {
|
||||
gen.writeNumber(floatValue);
|
||||
} else if (value instanceof Long longValue) {
|
||||
gen.writeNumber(longValue);
|
||||
} else {
|
||||
gen.writeNumber(value.intValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
package cn.iocoder.yudao.framework.common.util.json.databind;
|
||||
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.core.JsonParser;
|
||||
import tools.jackson.databind.DeserializationContext;
|
||||
import tools.jackson.databind.ValueDeserializer;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
@ -14,12 +14,12 @@ import java.time.ZoneId;
|
||||
*
|
||||
* @author 老五
|
||||
*/
|
||||
public class TimestampLocalDateTimeDeserializer extends ValueDeserializer<LocalDateTime> {
|
||||
public class TimestampLocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
|
||||
|
||||
public static final TimestampLocalDateTimeDeserializer INSTANCE = new TimestampLocalDateTimeDeserializer();
|
||||
|
||||
@Override
|
||||
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException {
|
||||
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
// 将 Long 时间戳,转换为 LocalDateTime 对象
|
||||
return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault());
|
||||
}
|
||||
|
||||
@ -5,12 +5,12 @@ 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 tools.jackson.core.JacksonException;
|
||||
import tools.jackson.core.JsonGenerator;
|
||||
import tools.jackson.databind.SerializationContext;
|
||||
import tools.jackson.databind.ser.std.StdScalarSerializer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
@ -25,22 +25,18 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
* @author 老五
|
||||
*/
|
||||
@Slf4j
|
||||
public class TimestampLocalDateTimeSerializer extends StdScalarSerializer<LocalDateTime> {
|
||||
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<>();
|
||||
|
||||
public TimestampLocalDateTimeSerializer() {
|
||||
super(LocalDateTime.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(LocalDateTime value, JsonGenerator gen, SerializationContext serializers) throws JacksonException {
|
||||
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||
// 情况一:有 JsonFormat 自定义注解,则使用它。https://github.com/YunaiV/ruoyi-vue-pro/pull/1019
|
||||
String fieldName = gen.streamWriteContext().currentName();
|
||||
String fieldName = gen.getOutputContext().getCurrentName();
|
||||
if (fieldName != null) {
|
||||
Object currentValue = gen.currentValue();
|
||||
Object currentValue = gen.getOutputContext().getCurrentValue();
|
||||
if (currentValue != null) {
|
||||
Class<?> clazz = currentValue.getClass();
|
||||
Map<String, Field> fieldMap = FIELD_CACHE.computeIfAbsent(clazz, this::buildFieldMap);
|
||||
|
||||
@ -26,9 +26,10 @@ public class ServletUtils {
|
||||
* @param response 响应
|
||||
* @param object 对象,会序列化成 JSON 字符串
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码
|
||||
public static void writeJSON(HttpServletResponse response, Object object) {
|
||||
String content = JsonUtils.toJsonString(object);
|
||||
JakartaServletUtil.write(response, content, MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8");
|
||||
JakartaServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接方法的参数
|
||||
*
|
||||
|
||||
@ -9,6 +9,36 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
*/
|
||||
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() {
|
||||
// 准备参数
|
||||
|
||||
@ -4,7 +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 org.dromara.trans.service.impl.SimpleTransService;
|
||||
import com.fhs.trans.service.impl.SimpleTransService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Collections;
|
||||
@ -65,7 +65,7 @@ public class DataPermissionRuleFactoryImpl implements DataPermissionRuleFactory
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为数据翻译 {@link org.dromara.core.trans.anno.Trans} 的调用
|
||||
* 判断是否为数据翻译 {@link com.fhs.core.trans.anno.Trans} 的调用
|
||||
*
|
||||
* 目前暂时只有这个办法,已经和 easy-trans 做过沟通
|
||||
*
|
||||
|
||||
@ -30,6 +30,7 @@ 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;
|
||||
@ -42,9 +43,12 @@ import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
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 java.util.*;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||
|
||||
@AutoConfiguration
|
||||
@ConditionalOnProperty(prefix = "yudao.tenant", value = "enable", matchIfMissing = true) // 允许使用 yudao.tenant.enable=false 禁用多租户
|
||||
@EnableConfigurationProperties(TenantProperties.class)
|
||||
@ -139,35 +143,50 @@ public class YudaoTenantAutoConfiguration {
|
||||
continue;
|
||||
}
|
||||
// 添加到忽略的 URL 中
|
||||
ignoreUrls.addAll(entry.getKey().getPatternValues());
|
||||
if (entry.getKey().getPatternsCondition() != null) {
|
||||
ignoreUrls.addAll(entry.getKey().getPatternsCondition().getPatterns());
|
||||
}
|
||||
if (entry.getKey().getPathPatternsCondition() != null) {
|
||||
ignoreUrls.addAll(
|
||||
convertList(entry.getKey().getPathPatternsCondition().getPatterns(), PathPattern::getPatternString));
|
||||
}
|
||||
}
|
||||
return ignoreUrls;
|
||||
}
|
||||
|
||||
// ========== MQ ==========
|
||||
|
||||
@Bean
|
||||
public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() {
|
||||
return new TenantRedisMessageInterceptor();
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(name = "cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor")
|
||||
public static class TenantRedisMQConfiguration {
|
||||
|
||||
@Bean
|
||||
public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() {
|
||||
return new TenantRedisMessageInterceptor();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate")
|
||||
public TenantRabbitMQInitializer tenantRabbitMQInitializer() {
|
||||
return new TenantRabbitMQInitializer();
|
||||
public static class TenantRabbitMQConfiguration {
|
||||
|
||||
@Bean
|
||||
public TenantRabbitMQInitializer tenantRabbitMQInitializer() {
|
||||
return new TenantRabbitMQInitializer();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(name = "org.apache.rocketmq.spring.core.RocketMQTemplate")
|
||||
public TenantRocketMQInitializer tenantRocketMQInitializer() {
|
||||
return new TenantRocketMQInitializer();
|
||||
}
|
||||
public static class TenantRocketMQConfiguration {
|
||||
|
||||
// ========== Job ==========
|
||||
@Bean
|
||||
public TenantRocketMQInitializer tenantRocketMQInitializer() {
|
||||
return new TenantRocketMQInitializer();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TenantJobAspect tenantJobAspect(TenantFrameworkService tenantFrameworkService) {
|
||||
return new TenantJobAspect(tenantFrameworkService);
|
||||
}
|
||||
|
||||
// ========== Redis ==========
|
||||
@ -183,7 +202,25 @@ 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;
|
||||
}
|
||||
|
||||
// ========== Job ==========
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(name = "cn.iocoder.yudao.framework.quartz.core.handler.JobHandler")
|
||||
public static class TenantJobConfiguration {
|
||||
|
||||
@Bean
|
||||
public TenantJobAspect tenantJobAspect(TenantFrameworkService tenantFrameworkService) {
|
||||
return new TenantJobAspect(tenantFrameworkService);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -2,8 +2,8 @@ package cn.iocoder.yudao.framework.tenant.core.mq.kafka;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.EnvironmentPostProcessor;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.env.EnvironmentPostProcessor;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
|
||||
/**
|
||||
|
||||
@ -27,7 +27,7 @@ import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.handler.HandlerMethod;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
org.springframework.boot.EnvironmentPostProcessor=\
|
||||
org.springframework.boot.env.EnvironmentPostProcessor=\
|
||||
cn.iocoder.yudao.framework.tenant.core.mq.kafka.TenantKafkaEnvironmentPostProcessor
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
<!-- Spring 核心 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aspectj</artifactId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Web 相关 -->
|
||||
|
||||
@ -2,7 +2,7 @@ package cn.iocoder.yudao.framework.tracer.config;
|
||||
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.micrometer.metrics.autoconfigure.MeterRegistryCustomizer;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package cn.iocoder.yudao.framework.mq.rabbitmq.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.amqp.support.converter.JacksonJsonMessageConverter;
|
||||
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
|
||||
import org.springframework.amqp.support.converter.MessageConverter;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
@ -18,11 +18,11 @@ import org.springframework.context.annotation.Bean;
|
||||
public class YudaoRabbitMQAutoConfiguration {
|
||||
|
||||
/**
|
||||
* JacksonJsonMessageConverter Bean:使用 jackson 序列化消息
|
||||
* Jackson2JsonMessageConverter Bean:使用 jackson 序列化消息
|
||||
*/
|
||||
@Bean
|
||||
public MessageConverter createMessageConverter() {
|
||||
return new JacksonJsonMessageConverter();
|
||||
return new Jackson2JsonMessageConverter();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -23,7 +23,16 @@ import java.util.List;
|
||||
@AllArgsConstructor
|
||||
public class RedisStreamMessageCleanupJob {
|
||||
|
||||
private static final String LOCK_KEY = "redis:stream:message-cleanup:lock";
|
||||
/**
|
||||
* 业务 MQ(Spring 容器内 AbstractRedisStreamMessageListener)清理任务使用的分布式锁
|
||||
*/
|
||||
public static final String DEFAULT_CLEANUP_LOCK_KEY = "redis:stream:message-cleanup:lock";
|
||||
|
||||
/**
|
||||
* IoT Redis 总线清理任务使用的分布式锁(须与 {@link #DEFAULT_CLEANUP_LOCK_KEY} 区分,否则会共抢一把锁,
|
||||
* 同一时刻只有一侧能执行 XTRIM,另一侧 Stream 可能无限积压)
|
||||
*/
|
||||
public static final String IOT_CLEANUP_LOCK_KEY = "redis:stream:message-cleanup:lock:iot";
|
||||
|
||||
/**
|
||||
* 保留的消息数量,默认保留最近 10000 条消息
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -71,11 +71,11 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>druid-spring-boot-4-starter</artifactId>
|
||||
<artifactId>druid-spring-boot-3-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot4-starter</artifactId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
@ -83,7 +83,7 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>dynamic-datasource-spring-boot4-starter</artifactId> <!-- 多数据源 -->
|
||||
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId> <!-- 多数据源 -->
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@ -98,13 +98,20 @@
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId> <!-- VO 数据翻译 -->
|
||||
<groupId>com.fhs-opensource</groupId> <!-- VO 数据翻译 -->
|
||||
<artifactId>easy-trans-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<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>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package cn.iocoder.yudao.framework.datasource.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.datasource.core.filter.DruidAdRemoveFilter;
|
||||
import com.alibaba.druid.spring.boot4.autoconfigure.properties.DruidStatProperties;
|
||||
import com.alibaba.druid.spring.boot3.autoconfigure.properties.DruidStatProperties;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
|
||||
@ -6,8 +6,8 @@ import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.EnvironmentPostProcessor;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.env.EnvironmentPostProcessor;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
|
||||
|
||||
@ -6,14 +6,16 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.handler.DefaultDBFieldHandler;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
|
||||
import com.baomidou.mybatisplus.core.handlers.IJsonTypeHandler;
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator;
|
||||
import com.baomidou.mybatisplus.extension.handlers.Jackson3TypeHandler;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import com.baomidou.mybatisplus.extension.incrementer.*;
|
||||
import com.baomidou.mybatisplus.extension.parser.JsqlParserGlobal;
|
||||
import com.baomidou.mybatisplus.extension.parser.cache.JdkSerialCaffeineJsqlParseCache;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
@ -23,7 +25,6 @@ import org.springframework.core.env.ConfigurableEnvironment;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* MyBaits 配置类
|
||||
@ -80,15 +81,15 @@ public class YudaoMybatisAutoConfiguration {
|
||||
throw new IllegalArgumentException(StrUtil.format("DbType{} 找不到合适的 IKeyGenerator 实现类", dbType));
|
||||
}
|
||||
|
||||
@Bean // 特殊:返回结果使用 Object 而不用 Jackson3TypeHandler 的原因,避免因为 Jackson3TypeHandler 被 mybatis 全局使用!
|
||||
@Bean // 特殊:返回结果使用 Object 而不用 JacksonTypeHandler 的原因,避免因为 JacksonTypeHandler 被 mybatis 全局使用!
|
||||
public Object jacksonTypeHandler(List<ObjectMapper> objectMappers) {
|
||||
// 特殊:设置 Jackson3TypeHandler 的 ObjectMapper!
|
||||
// 特殊:设置 JacksonTypeHandler 的 ObjectMapper!
|
||||
ObjectMapper objectMapper = CollUtil.getFirst(objectMappers);
|
||||
if (objectMapper == null) {
|
||||
objectMapper = JsonUtils.getObjectMapper();
|
||||
}
|
||||
Jackson3TypeHandler.setObjectMapper(objectMapper);
|
||||
return new Jackson3TypeHandler(Object.class);
|
||||
JacksonTypeHandler.setObjectMapper(objectMapper);
|
||||
return new JacksonTypeHandler(Object.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import org.dromara.core.trans.vo.TransPojo;
|
||||
import com.fhs.core.trans.vo.TransPojo;
|
||||
import lombok.Data;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -170,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<>());
|
||||
|
||||
@ -25,6 +25,13 @@ 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)) {
|
||||
return (LambdaQueryWrapperX<T>) super.in(column, values);
|
||||
|
||||
@ -27,6 +27,13 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
|
||||
return this;
|
||||
}
|
||||
|
||||
public <S> MPJLambdaWrapperX<T> likeRightIfPresent(SFunction<S, ?> column, String val) {
|
||||
if (StringUtils.hasText(val)) {
|
||||
return (MPJLambdaWrapperX<T>) super.likeRight(column, val);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public <S> MPJLambdaWrapperX<T> inIfPresent(SFunction<S, ?> column, Collection<?> values) {
|
||||
if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
|
||||
return (MPJLambdaWrapperX<T>) super.in(column, values);
|
||||
@ -102,7 +109,6 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
// ========== 重写父类方法,方便链式调用 ==========
|
||||
|
||||
@Override
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -42,8 +49,11 @@ public class MyBatisUtils {
|
||||
// 排序字段
|
||||
if (CollUtil.isNotEmpty(sortingFields)) {
|
||||
for (SortingField sortingField : sortingFields) {
|
||||
page.addOrder(new OrderItem().setAsc(SortingField.ORDER_ASC.equals(sortingField.getOrder()))
|
||||
.setColumn(StrUtil.toUnderlineCase(sortingField.getField())));
|
||||
String columnName = buildSafeOrderColumn(sortingField.getField());
|
||||
if (columnName == null) {
|
||||
continue;
|
||||
}
|
||||
page.addOrder(new OrderItem().setAsc(isAscOrder(sortingField.getOrder())).setColumn(columnName));
|
||||
}
|
||||
}
|
||||
return page;
|
||||
@ -57,23 +67,29 @@ public class MyBatisUtils {
|
||||
if (wrapper instanceof QueryWrapper<T>) {
|
||||
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<T>) {
|
||||
// LambdaQueryWrapper 不直接支持字符串字段排序,使用 last 方法拼接 ORDER BY
|
||||
LambdaQueryWrapper<T> lambdaQuery = (LambdaQueryWrapper<T>) wrapper;
|
||||
StringBuilder orderBy = new StringBuilder();
|
||||
for (SortingField sortingField : sortingFields) {
|
||||
String columnName = buildSafeOrderColumn(sortingField.getField());
|
||||
if (columnName == null) {
|
||||
continue;
|
||||
}
|
||||
if (StrUtil.isNotEmpty(orderBy)) {
|
||||
orderBy.append(", ");
|
||||
}
|
||||
orderBy.append(StrUtil.toUnderlineCase(sortingField.getField()))
|
||||
.append(" ")
|
||||
.append(SortingField.ORDER_ASC.equals(sortingField.getOrder()) ? "ASC" : "DESC");
|
||||
orderBy.append(columnName).append(" ").append(getOrderDirection(sortingField.getOrder()));
|
||||
}
|
||||
if (StrUtil.isNotEmpty(orderBy)) {
|
||||
lambdaQuery.last("ORDER BY " + orderBy);
|
||||
}
|
||||
lambdaQuery.last("ORDER BY " + orderBy);
|
||||
// 另外个思路:https://blog.csdn.net/m0_59084856/article/details/138450913
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported wrapper type: " + wrapper.getClass().getName());
|
||||
@ -81,6 +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 不支持添加拦截器,所以只能全量设置
|
||||
@ -129,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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
package cn.iocoder.yudao.framework.translate.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.translate.core.TranslateUtils;
|
||||
import org.dromara.trans.service.impl.TransService;
|
||||
import com.fhs.trans.service.impl.TransService;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
@AutoConfiguration
|
||||
public class YudaoTranslateAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(TransService.class)
|
||||
@SuppressWarnings({"InstantiationOfUtilityClass", "SpringJavaInjectionPointsAutowiringInspection"})
|
||||
public TranslateUtils translateUtils(TransService transService) {
|
||||
TranslateUtils.init(transService);
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package cn.iocoder.yudao.framework.translate.core;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import org.dromara.core.trans.vo.VO;
|
||||
import org.dromara.trans.service.impl.TransService;
|
||||
import com.fhs.core.trans.vo.VO;
|
||||
import com.fhs.trans.service.impl.TransService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
org.springframework.boot.EnvironmentPostProcessor=\
|
||||
org.springframework.boot.env.EnvironmentPostProcessor=\
|
||||
cn.iocoder.yudao.framework.mybatis.config.IdTypeEnvironmentPostProcessor
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@ -33,8 +33,8 @@
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-jackson</artifactId>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ package cn.iocoder.yudao.framework.redis.config;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.redis.core.TimeoutRedisCacheManager;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.cache.autoconfigure.CacheProperties;
|
||||
import org.springframework.boot.autoconfigure.cache.CacheProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,18 +1,19 @@
|
||||
package cn.iocoder.yudao.framework.redis.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import org.redisson.spring.starter.RedissonAutoConfigurationV4;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.redisson.spring.starter.RedissonAutoConfigurationV2;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
|
||||
/**
|
||||
* Redis 配置类
|
||||
*/
|
||||
@AutoConfiguration(before = RedissonAutoConfigurationV4.class) // 目的:使用自己定义的 RedisTemplate Bean
|
||||
@AutoConfiguration(before = RedissonAutoConfigurationV2.class) // 目的:使用自己定义的 RedisTemplate Bean
|
||||
public class YudaoRedisAutoConfiguration {
|
||||
|
||||
/**
|
||||
@ -27,15 +28,18 @@ public class YudaoRedisAutoConfiguration {
|
||||
// 使用 String 序列化方式,序列化 KEY 。
|
||||
template.setKeySerializer(RedisSerializer.string());
|
||||
template.setHashKeySerializer(RedisSerializer.string());
|
||||
// 使用 JSON 序列化方式,序列化 VALUE
|
||||
RedisSerializer<?> redisSerializer = buildRedisSerializer();
|
||||
template.setValueSerializer(redisSerializer);
|
||||
template.setHashValueSerializer(redisSerializer);
|
||||
// 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。
|
||||
template.setValueSerializer(buildRedisSerializer());
|
||||
template.setHashValueSerializer(buildRedisSerializer());
|
||||
return template;
|
||||
}
|
||||
|
||||
public static RedisSerializer<?> buildRedisSerializer() {
|
||||
return new GenericJacksonJsonRedisSerializer(JsonUtils.getObjectMapper());
|
||||
RedisSerializer<Object> json = RedisSerializer.json();
|
||||
// 解决 LocalDateTime 的序列化
|
||||
ObjectMapper objectMapper = (ObjectMapper) ReflectUtil.getFieldValue(json, "mapper");
|
||||
objectMapper.registerModules(new JavaTimeModule());
|
||||
return json;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
<!-- Spring 核心 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aspectj</artifactId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Web 相关 -->
|
||||
|
||||
@ -29,12 +29,15 @@ import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
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 java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||
|
||||
/**
|
||||
* 自定义的 Spring Security 配置适配器实现
|
||||
*
|
||||
@ -167,7 +170,12 @@ public class YudaoWebSecurityConfigurerAdapter {
|
||||
continue;
|
||||
}
|
||||
Set<String> urls = new HashSet<>();
|
||||
urls.addAll(entry.getKey().getPatternValues());
|
||||
if (entry.getKey().getPatternsCondition() != null) {
|
||||
urls.addAll(entry.getKey().getPatternsCondition().getPatterns());
|
||||
}
|
||||
if (entry.getKey().getPathPatternsCondition() != null) {
|
||||
urls.addAll(convertList(entry.getKey().getPathPatternsCondition().getPatterns(), PathPattern::getPatternString));
|
||||
}
|
||||
if (urls.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.security.core.LoginUser;
|
||||
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package cn.iocoder.yudao.framework.test.config;
|
||||
|
||||
import com.github.fppt.jedismock.RedisServer;
|
||||
import org.springframework.boot.data.redis.autoconfigure.DataRedisProperties;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@ -16,14 +16,14 @@ import java.io.IOException;
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Lazy(false) // 禁止延迟加载
|
||||
@EnableConfigurationProperties(DataRedisProperties.class)
|
||||
@EnableConfigurationProperties(RedisProperties.class)
|
||||
public class RedisTestConfiguration {
|
||||
|
||||
/**
|
||||
* 创建模拟的 Redis Server 服务器
|
||||
*/
|
||||
@Bean
|
||||
public RedisServer redisServer(DataRedisProperties properties) throws IOException {
|
||||
public RedisServer redisServer(RedisProperties properties) throws IOException {
|
||||
RedisServer redisServer = new RedisServer(properties.getPort());
|
||||
// 一次执行多个单元测试时,貌似创建多个 spring 容器,导致不进行 stop。这样,就导致端口被占用,无法启动。。。
|
||||
try {
|
||||
|
||||
@ -3,7 +3,7 @@ package cn.iocoder.yudao.framework.test.config;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
|
||||
import org.springframework.boot.sql.autoconfigure.init.SqlInitializationProperties;
|
||||
import org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
|
||||
import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer;
|
||||
@ -17,7 +17,7 @@ import javax.sql.DataSource;
|
||||
/**
|
||||
* SQL 初始化的测试 Configuration
|
||||
*
|
||||
* 为什么不使用 org.springframework.boot.sql.autoconfigure.init.DataSourceInitializationConfiguration 呢?
|
||||
* 为什么不使用 org.springframework.boot.autoconfigure.sql.init.DataSourceInitializationConfiguration 呢?
|
||||
* 因为我们在单元测试会使用 spring.main.lazy-initialization 为 true,开启延迟加载。此时,会导致 DataSourceInitializationConfiguration 初始化
|
||||
* 不过呢,当前类的实现代码,基本是复制 DataSourceInitializationConfiguration 的哈!
|
||||
*
|
||||
|
||||
@ -6,12 +6,12 @@ import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration;
|
||||
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
|
||||
import cn.iocoder.yudao.framework.test.config.RedisTestConfiguration;
|
||||
import cn.iocoder.yudao.framework.test.config.SqlInitializationTestConfiguration;
|
||||
import com.alibaba.druid.spring.boot4.autoconfigure.DruidDataSourceAutoConfigure;
|
||||
import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure;
|
||||
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
|
||||
import org.redisson.spring.starter.RedissonAutoConfigurationV4;
|
||||
import org.springframework.boot.data.redis.autoconfigure.DataRedisAutoConfiguration;
|
||||
import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration;
|
||||
import org.springframework.boot.jdbc.autoconfigure.DataSourceTransactionManagerAutoConfiguration;
|
||||
import org.redisson.spring.starter.RedissonAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
@ -43,8 +43,8 @@ public class BaseDbAndRedisUnitTest {
|
||||
// Redis 配置类
|
||||
RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer
|
||||
YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类
|
||||
DataRedisAutoConfiguration.class, // Spring Redis 自动配置类
|
||||
RedissonAutoConfigurationV4.class, // Redisson 自动配置类
|
||||
RedisAutoConfiguration.class, // Spring Redis 自动配置类
|
||||
RedissonAutoConfiguration.class, // Redisson 自动配置类
|
||||
|
||||
// 其它配置类
|
||||
SpringUtil.class
|
||||
|
||||
@ -4,11 +4,11 @@ import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration;
|
||||
import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration;
|
||||
import cn.iocoder.yudao.framework.test.config.SqlInitializationTestConfiguration;
|
||||
import com.alibaba.druid.spring.boot4.autoconfigure.DruidDataSourceAutoConfigure;
|
||||
import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure;
|
||||
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
|
||||
import com.github.yulichang.autoconfigure.MybatisPlusJoinAutoConfiguration;
|
||||
import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration;
|
||||
import org.springframework.boot.jdbc.autoconfigure.DataSourceTransactionManagerAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
@ -3,8 +3,8 @@ package cn.iocoder.yudao.framework.test.core.ut;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
|
||||
import cn.iocoder.yudao.framework.test.config.RedisTestConfiguration;
|
||||
import org.redisson.spring.starter.RedissonAutoConfigurationV4;
|
||||
import org.springframework.boot.data.redis.autoconfigure.DataRedisAutoConfiguration;
|
||||
import org.redisson.spring.starter.RedissonAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
@ -23,9 +23,9 @@ public class BaseRedisUnitTest {
|
||||
@Import({
|
||||
// Redis 配置类
|
||||
RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer
|
||||
DataRedisAutoConfiguration.class, // Spring Redis 自动配置类
|
||||
RedisAutoConfiguration.class, // Spring Redis 自动配置类
|
||||
YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类
|
||||
RedissonAutoConfigurationV4.class, // Redisson 自动配置类
|
||||
RedissonAutoConfiguration.class, // Redisson 自动配置类
|
||||
|
||||
// 其它配置类
|
||||
SpringUtil.class
|
||||
|
||||
@ -26,14 +26,6 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-jackson</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-restclient</artifactId>
|
||||
</dependency>
|
||||
<!-- spring boot 配置所需依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
@ -19,6 +19,7 @@ import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
|
||||
import cn.iocoder.yudao.framework.web.config.WebProperties;
|
||||
import cn.iocoder.yudao.framework.web.core.filter.ApiRequestFilter;
|
||||
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.FilterChain;
|
||||
@ -28,7 +29,6 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@ -3,7 +3,7 @@ package cn.iocoder.yudao.framework.desensitize.core.base.annotation;
|
||||
import cn.iocoder.yudao.framework.desensitize.core.base.handler.DesensitizationHandler;
|
||||
import cn.iocoder.yudao.framework.desensitize.core.base.serializer.StringDesensitizeSerializer;
|
||||
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
|
||||
import tools.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
|
||||
@ -7,15 +7,16 @@ import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy;
|
||||
import cn.iocoder.yudao.framework.desensitize.core.base.handler.DesensitizationHandler;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.BeanProperty;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.core.JsonGenerator;
|
||||
import tools.jackson.databind.BeanProperty;
|
||||
import tools.jackson.databind.SerializationContext;
|
||||
import tools.jackson.databind.ValueSerializer;
|
||||
import tools.jackson.databind.ser.std.StdSerializer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
@ -27,7 +28,7 @@ import java.lang.reflect.Field;
|
||||
* @author gaibu
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class StringDesensitizeSerializer extends StdSerializer<String> {
|
||||
public class StringDesensitizeSerializer extends StdSerializer<String> implements ContextualSerializer {
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@ -38,7 +39,7 @@ public class StringDesensitizeSerializer extends StdSerializer<String> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValueSerializer<?> createContextual(SerializationContext serializerProvider, BeanProperty beanProperty) {
|
||||
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) {
|
||||
DesensitizeBy annotation = beanProperty.getAnnotation(DesensitizeBy.class);
|
||||
if (annotation == null) {
|
||||
return this;
|
||||
@ -51,7 +52,7 @@ public class StringDesensitizeSerializer extends StdSerializer<String> {
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void serialize(String value, JsonGenerator gen, SerializationContext serializerProvider) throws JacksonException {
|
||||
public void serialize(String value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException {
|
||||
if (StrUtil.isBlank(value)) {
|
||||
gen.writeNull();
|
||||
return;
|
||||
@ -82,7 +83,7 @@ public class StringDesensitizeSerializer extends StdSerializer<String> {
|
||||
* @return 字段
|
||||
*/
|
||||
private Field getField(JsonGenerator generator) {
|
||||
String currentName = generator.streamWriteContext().currentName();
|
||||
String currentName = generator.getOutputContext().getCurrentName();
|
||||
Object currentValue = generator.currentValue();
|
||||
Class<?> currentValueClass = currentValue.getClass();
|
||||
return ReflectUtil.getField(currentValueClass, currentName);
|
||||
|
||||
@ -4,18 +4,18 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.json.databind.NumberSerializer;
|
||||
import cn.iocoder.yudao.framework.common.util.json.databind.TimestampLocalDateTimeDeserializer;
|
||||
import cn.iocoder.yudao.framework.common.util.json.databind.TimestampLocalDateTimeSerializer;
|
||||
import com.fasterxml.jackson.databind.Module;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration;
|
||||
import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer;
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import tools.jackson.databind.JacksonModule;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.ext.javatime.deser.LocalDateDeserializer;
|
||||
import tools.jackson.databind.ext.javatime.deser.LocalTimeDeserializer;
|
||||
import tools.jackson.databind.ext.javatime.ser.LocalDateSerializer;
|
||||
import tools.jackson.databind.ext.javatime.ser.LocalTimeSerializer;
|
||||
import tools.jackson.databind.module.SimpleModule;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
@ -29,15 +29,26 @@ public class YudaoJacksonAutoConfiguration {
|
||||
* 从 Builder 源头定制(关键:使用 *ByType,避免 handledType 要求)
|
||||
*/
|
||||
@Bean
|
||||
public JsonMapperBuilderCustomizer ldtEpochMillisCustomizer(JacksonModule timestampSupportModuleBean) {
|
||||
return builder -> builder.addModule(timestampSupportModuleBean);
|
||||
public Jackson2ObjectMapperBuilderCustomizer ldtEpochMillisCustomizer() {
|
||||
return builder -> builder
|
||||
// Long -> Number
|
||||
.serializerByType(Long.class, NumberSerializer.INSTANCE)
|
||||
.serializerByType(Long.TYPE, NumberSerializer.INSTANCE)
|
||||
// LocalDate / LocalTime
|
||||
.serializerByType(LocalDate.class, LocalDateSerializer.INSTANCE)
|
||||
.deserializerByType(LocalDate.class, LocalDateDeserializer.INSTANCE)
|
||||
.serializerByType(LocalTime.class, LocalTimeSerializer.INSTANCE)
|
||||
.deserializerByType(LocalTime.class, LocalTimeDeserializer.INSTANCE)
|
||||
// LocalDateTime < - > EpochMillis
|
||||
.serializerByType(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE)
|
||||
.deserializerByType(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 以 Bean 形式暴露 Module(Boot 会自动注册到所有 ObjectMapper)
|
||||
*/
|
||||
@Bean
|
||||
public JacksonModule timestampSupportModuleBean() {
|
||||
public Module timestampSupportModuleBean() {
|
||||
SimpleModule m = new SimpleModule("TimestampSupportModule");
|
||||
// Long -> Number,避免前端精度丢失
|
||||
m.addSerializer(Long.class, NumberSerializer.INSTANCE);
|
||||
|
||||
@ -14,9 +14,10 @@ import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.webmvc.autoconfigure.WebMvcRegistrations;
|
||||
import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.restclient.RestTemplateBuilder;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.annotation.Order;
|
||||
@ -143,7 +144,7 @@ public class YudaoWebAutoConfiguration {
|
||||
/**
|
||||
* 创建 RestTemplate 实例
|
||||
*
|
||||
* @param restTemplateBuilder {@link RestTemplateBuilder#build}
|
||||
* @param restTemplateBuilder {@link RestTemplateAutoConfiguration#restTemplateBuilder}
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
|
||||
@ -15,6 +15,7 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
|
||||
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
|
||||
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
|
||||
import com.google.common.util.concurrent.UncheckedExecutionException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
@ -38,7 +39,6 @@ import org.springframework.web.method.annotation.MethodArgumentTypeMismatchExcep
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
import tools.jackson.databind.exc.InvalidFormatException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@ -9,7 +9,7 @@ import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer;
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@ -37,17 +37,17 @@ public class YudaoXssAutoConfiguration implements WebMvcConfigurer {
|
||||
/**
|
||||
* 注册 Jackson 的序列化器,用于处理 json 类型参数的 xss 过滤
|
||||
*
|
||||
* @return JsonMapperBuilderCustomizer
|
||||
* @return Jackson2ObjectMapperBuilderCustomizer
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = "xssJacksonCustomizer")
|
||||
@ConditionalOnProperty(value = "yudao.xss.enable", havingValue = "true")
|
||||
public JsonMapperBuilderCustomizer xssJacksonCustomizer(XssProperties properties,
|
||||
PathMatcher pathMatcher,
|
||||
XssCleaner xssCleaner) {
|
||||
public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(XssProperties properties,
|
||||
PathMatcher pathMatcher,
|
||||
XssCleaner xssCleaner) {
|
||||
// 在反序列化时进行 xss 过滤,可以替换使用 XssStringJsonSerializer,在序列化时进行处理
|
||||
return builder -> builder.addModule(new tools.jackson.databind.module.SimpleModule("XssStringModule")
|
||||
.addDeserializer(String.class, new XssStringJsonDeserializer(properties, pathMatcher, xssCleaner)));
|
||||
return builder ->
|
||||
builder.deserializerByType(String.class, new XssStringJsonDeserializer(properties, pathMatcher, xssCleaner));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -3,15 +3,16 @@ package cn.iocoder.yudao.framework.xss.core.json;
|
||||
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
|
||||
import cn.iocoder.yudao.framework.xss.config.XssProperties;
|
||||
import cn.iocoder.yudao.framework.xss.core.clean.XssCleaner;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonToken;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.deser.std.StringDeserializer;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.PathMatcher;
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.core.JsonParser;
|
||||
import tools.jackson.core.JsonToken;
|
||||
import tools.jackson.databind.DeserializationContext;
|
||||
import tools.jackson.databind.deser.jdk.StringDeserializer;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* XSS 过滤 jackson 反序列化器。
|
||||
@ -35,19 +36,19 @@ public class XssStringJsonDeserializer extends StringDeserializer {
|
||||
private final XssCleaner xssCleaner;
|
||||
|
||||
@Override
|
||||
public String deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException {
|
||||
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
// 1. 白名单 URL 的处理
|
||||
HttpServletRequest request = ServletUtils.getRequest();
|
||||
if (request != null) {
|
||||
String uri = ServletUtils.getRequest().getRequestURI();
|
||||
if (properties.getExcludeUrls().stream().anyMatch(excludeUrl -> pathMatcher.match(excludeUrl, uri))) {
|
||||
return p.getString();
|
||||
return p.getText();
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 真正使用 xssCleaner 进行过滤
|
||||
if (p.hasToken(JsonToken.VALUE_STRING)) {
|
||||
return xssCleaner.clean(p.getString());
|
||||
return xssCleaner.clean(p.getText());
|
||||
}
|
||||
JsonToken t = p.currentToken();
|
||||
// [databind#381]
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -19,10 +19,9 @@
|
||||
国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno
|
||||
</description>
|
||||
<properties>
|
||||
<spring-ai.version>2.0.0-M7</spring-ai.version>
|
||||
<spring-ai-legacy-model.version>2.0.0-M4</spring-ai-legacy-model.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.3</alibaba-ai.version>
|
||||
<alibaba-ai.version>1.1.2.2</alibaba-ai.version>
|
||||
<tinyflow.version>1.2.6</tinyflow.version>
|
||||
</properties>
|
||||
|
||||
@ -90,7 +89,7 @@
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-model-azure-openai</artifactId>
|
||||
<version>${spring-ai-legacy-model.version}</version>
|
||||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
@ -116,7 +115,7 @@
|
||||
<!-- 智谱 GLM -->
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-model-zhipuai</artifactId>
|
||||
<version>${spring-ai-legacy-model.version}</version>
|
||||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
@ -134,13 +133,13 @@
|
||||
<dependency>
|
||||
<!-- 文心一言 -->
|
||||
<groupId>org.springaicommunity</groupId>
|
||||
<artifactId>qianfan-core</artifactId>
|
||||
<artifactId>qianfan-spring-boot-starter</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<!-- 月之暗面 -->
|
||||
<groupId>org.springaicommunity</groupId>
|
||||
<artifactId>moonshot-core</artifactId>
|
||||
<artifactId>moonshot-spring-boot-starter</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
|
||||
@ -212,22 +211,12 @@
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-autoconfigure-mcp-server-common</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<!-- 客户端 -->
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-mcp-client</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-autoconfigure-mcp-client-common</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- TinyFlow:AI 工作流 -->
|
||||
<dependency>
|
||||
@ -271,4 +260,4 @@
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
</project>
|
||||
@ -12,7 +12,7 @@ import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatCo
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
|
||||
import cn.iocoder.yudao.module.ai.service.chat.AiChatConversationService;
|
||||
import cn.iocoder.yudao.module.ai.service.chat.AiChatMessageService;
|
||||
import org.dromara.core.trans.anno.TransMethodResult;
|
||||
import com.fhs.core.trans.anno.TransMethodResult;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@ -2,9 +2,9 @@ package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation;
|
||||
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
|
||||
import org.dromara.core.trans.anno.Trans;
|
||||
import org.dromara.core.trans.constant.TransType;
|
||||
import org.dromara.core.trans.vo.VO;
|
||||
import com.fhs.core.trans.anno.Trans;
|
||||
import com.fhs.core.trans.constant.TransType;
|
||||
import com.fhs.core.trans.vo.VO;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleS
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleSaveReqVO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
|
||||
import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
|
||||
import org.dromara.core.trans.anno.TransMethodResult;
|
||||
import com.fhs.core.trans.anno.TransMethodResult;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
package cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole;
|
||||
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
|
||||
import org.dromara.core.trans.anno.Trans;
|
||||
import org.dromara.core.trans.constant.TransType;
|
||||
import org.dromara.core.trans.vo.VO;
|
||||
import com.fhs.core.trans.anno.Trans;
|
||||
import com.fhs.core.trans.constant.TransType;
|
||||
import com.fhs.core.trans.vo.VO;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@ -64,4 +64,4 @@ public class AiChatRoleRespVO implements VO {
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
}
|
||||
}
|
||||
@ -11,7 +11,7 @@ import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.Jackson3TypeHandler;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
@ -114,7 +114,7 @@ public class AiChatMessageDO extends BaseDO {
|
||||
/**
|
||||
* 联网搜索的网页内容数组
|
||||
*/
|
||||
@TableField(typeHandler = Jackson3TypeHandler.class)
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<AiWebSearchResponse.WebPage> webSearchPages;
|
||||
|
||||
/**
|
||||
|
||||
@ -10,7 +10,7 @@ import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.Jackson3TypeHandler;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import lombok.Data;
|
||||
import org.springframework.ai.openai.OpenAiImageOptions;
|
||||
import org.springframework.ai.stabilityai.api.StabilityAiImageOptions;
|
||||
@ -107,13 +107,13 @@ public class AiImageDO extends BaseDO {
|
||||
* 1. {@link OpenAiImageOptions}
|
||||
* 2. {@link StabilityAiImageOptions}
|
||||
*/
|
||||
@TableField(typeHandler = Jackson3TypeHandler.class)
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private Map<String, Object> options;
|
||||
|
||||
/**
|
||||
* mj buttons 按钮
|
||||
*/
|
||||
@TableField(typeHandler = Jackson3TypeHandler.class)
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<MidjourneyApi.Button> buttons;
|
||||
|
||||
/**
|
||||
|
||||
@ -8,7 +8,7 @@ import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.Jackson3TypeHandler;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
@ -93,7 +93,7 @@ public class AiMusicDO extends BaseDO {
|
||||
/**
|
||||
* 音乐风格标签
|
||||
*/
|
||||
@TableField(typeHandler = Jackson3TypeHandler.class)
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<String> tags;
|
||||
|
||||
/**
|
||||
|
||||
@ -14,7 +14,6 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlo
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel;
|
||||
import com.openai.client.okhttp.OpenAIOkHttpClient;
|
||||
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;
|
||||
@ -29,6 +28,7 @@ import org.springframework.ai.embedding.TokenCountBatchingStrategy;
|
||||
import org.springframework.ai.model.tool.ToolCallingManager;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.ai.support.ToolCallbacks;
|
||||
import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
|
||||
import org.springframework.ai.tokenizer.TokenCountEstimator;
|
||||
@ -86,11 +86,12 @@ public class AiAutoConfiguration {
|
||||
properties.setModel(GeminiChatModel.MODEL_DEFAULT);
|
||||
}
|
||||
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
|
||||
.openAiClient(OpenAIOkHttpClient.builder()
|
||||
.openAiApi(OpenAiApi.builder()
|
||||
.baseUrl(GeminiChatModel.BASE_URL)
|
||||
.completionsPath(GeminiChatModel.COMPLETE_PATH)
|
||||
.apiKey(properties.getApiKey())
|
||||
.build())
|
||||
.options(OpenAiChatOptions.builder()
|
||||
.defaultOptions(OpenAiChatOptions.builder()
|
||||
.model(properties.getModel())
|
||||
.temperature(properties.getTemperature())
|
||||
.maxTokens(properties.getMaxTokens())
|
||||
@ -113,11 +114,12 @@ public class AiAutoConfiguration {
|
||||
properties.setModel(DouBaoChatModel.MODEL_DEFAULT);
|
||||
}
|
||||
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
|
||||
.openAiClient(OpenAIOkHttpClient.builder()
|
||||
.openAiApi(OpenAiApi.builder()
|
||||
.baseUrl(DouBaoChatModel.BASE_URL)
|
||||
.completionsPath(DouBaoChatModel.COMPLETE_PATH)
|
||||
.apiKey(properties.getApiKey())
|
||||
.build())
|
||||
.options(OpenAiChatOptions.builder()
|
||||
.defaultOptions(OpenAiChatOptions.builder()
|
||||
.model(properties.getModel())
|
||||
.temperature(properties.getTemperature())
|
||||
.maxTokens(properties.getMaxTokens())
|
||||
@ -201,15 +203,16 @@ public class AiAutoConfiguration {
|
||||
if (StrUtil.isEmpty(properties.getModel())) {
|
||||
properties.setModel(XingHuoChatModel.MODEL_DEFAULT);
|
||||
}
|
||||
OpenAIOkHttpClient.Builder builder = OpenAIOkHttpClient.builder()
|
||||
OpenAiApi.Builder builder = OpenAiApi.builder()
|
||||
.baseUrl(XingHuoChatModel.BASE_URL_V1)
|
||||
.apiKey(properties.getAppKey() + ":" + properties.getSecretKey());
|
||||
if ("x1".equals(properties.getModel())) {
|
||||
builder.baseUrl(XingHuoChatModel.BASE_URL_V2);
|
||||
builder.baseUrl(XingHuoChatModel.BASE_URL_V2)
|
||||
.completionsPath(XingHuoChatModel.BASE_COMPLETIONS_PATH_V2);
|
||||
}
|
||||
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
|
||||
.openAiClient(builder.build())
|
||||
.options(OpenAiChatOptions.builder()
|
||||
.openAiApi(builder.build())
|
||||
.defaultOptions(OpenAiChatOptions.builder()
|
||||
.model(properties.getModel())
|
||||
.temperature(properties.getTemperature())
|
||||
.maxTokens(properties.getMaxTokens())
|
||||
@ -233,11 +236,11 @@ public class AiAutoConfiguration {
|
||||
properties.setModel(BaiChuanChatModel.MODEL_DEFAULT);
|
||||
}
|
||||
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
|
||||
.openAiClient(OpenAIOkHttpClient.builder()
|
||||
.openAiApi(OpenAiApi.builder()
|
||||
.baseUrl(BaiChuanChatModel.BASE_URL)
|
||||
.apiKey(properties.getApiKey())
|
||||
.build())
|
||||
.options(OpenAiChatOptions.builder()
|
||||
.defaultOptions(OpenAiChatOptions.builder()
|
||||
.model(properties.getModel())
|
||||
.temperature(properties.getTemperature())
|
||||
.maxTokens(properties.getMaxTokens())
|
||||
@ -266,12 +269,13 @@ public class AiAutoConfiguration {
|
||||
properties.setModel(GrokChatModel.MODEL_DEFAULT);
|
||||
}
|
||||
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
|
||||
.openAiClient(OpenAIOkHttpClient.builder()
|
||||
.openAiApi(OpenAiApi.builder()
|
||||
.baseUrl(Optional.ofNullable(properties.getBaseUrl())
|
||||
.orElse(GrokChatModel.BASE_URL))
|
||||
.completionsPath(GrokChatModel.COMPLETE_PATH)
|
||||
.apiKey(properties.getApiKey())
|
||||
.build())
|
||||
.options(OpenAiChatOptions.builder()
|
||||
.defaultOptions(OpenAiChatOptions.builder()
|
||||
.model(properties.getModel())
|
||||
.temperature(properties.getTemperature())
|
||||
.maxTokens(properties.getMaxTokens())
|
||||
@ -316,4 +320,4 @@ public class AiAutoConfiguration {
|
||||
return List.of(ToolCallbacks.from(personService));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -30,14 +30,11 @@ import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
|
||||
import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi;
|
||||
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
|
||||
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
|
||||
import com.alibaba.cloud.ai.dashscope.embedding.text.DashScopeEmbeddingModel;
|
||||
import com.alibaba.cloud.ai.dashscope.embedding.text.DashScopeEmbeddingOptions;
|
||||
import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel;
|
||||
import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingOptions;
|
||||
import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel;
|
||||
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
|
||||
import com.azure.ai.openai.OpenAIClientBuilder;
|
||||
import com.azure.core.credential.KeyCredential;
|
||||
import com.openai.client.OpenAIClient;
|
||||
import com.openai.client.okhttp.OpenAIOkHttpClient;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import io.milvus.client.MilvusServiceClient;
|
||||
import io.qdrant.client.QdrantClient;
|
||||
@ -46,12 +43,15 @@ import lombok.SneakyThrows;
|
||||
import org.springaicommunity.moonshot.MoonshotChatModel;
|
||||
import org.springaicommunity.moonshot.MoonshotChatOptions;
|
||||
import org.springaicommunity.moonshot.api.MoonshotApi;
|
||||
import org.springaicommunity.moonshot.autoconfigure.MoonshotChatAutoConfiguration;
|
||||
import org.springaicommunity.qianfan.QianFanChatModel;
|
||||
import org.springaicommunity.qianfan.QianFanEmbeddingModel;
|
||||
import org.springaicommunity.qianfan.QianFanEmbeddingOptions;
|
||||
import org.springaicommunity.qianfan.QianFanImageModel;
|
||||
import org.springaicommunity.qianfan.api.QianFanApi;
|
||||
import org.springaicommunity.qianfan.api.QianFanImageApi;
|
||||
import org.springaicommunity.qianfan.autoconfigure.QianFanChatAutoConfiguration;
|
||||
import org.springaicommunity.qianfan.autoconfigure.QianFanEmbeddingAutoConfiguration;
|
||||
import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
|
||||
import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
@ -92,7 +92,11 @@ import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.ai.openai.OpenAiEmbeddingModel;
|
||||
import org.springframework.ai.openai.OpenAiEmbeddingOptions;
|
||||
import org.springframework.ai.openai.OpenAiImageModel;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.ai.openai.api.OpenAiImageApi;
|
||||
import org.springframework.ai.openai.api.common.OpenAiApiConstants;
|
||||
import org.springframework.ai.anthropic.AnthropicChatModel;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi;
|
||||
import org.springframework.ai.stabilityai.StabilityAiImageModel;
|
||||
import org.springframework.ai.stabilityai.api.StabilityAiApi;
|
||||
import org.springframework.ai.vectorstore.SimpleVectorStore;
|
||||
@ -115,7 +119,7 @@ import org.springframework.ai.zhipuai.api.ZhiPuAiApi;
|
||||
import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.data.redis.autoconfigure.DataRedisProperties;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import redis.clients.jedis.JedisPooled;
|
||||
|
||||
@ -127,6 +131,7 @@ import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||
import static org.springframework.ai.retry.RetryUtils.DEFAULT_RETRY_TEMPLATE;
|
||||
|
||||
/**
|
||||
* AI Model 模型工厂的实现类
|
||||
@ -343,13 +348,9 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
*/
|
||||
private static DashScopeChatModel buildTongYiChatModel(String key) {
|
||||
DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(key).build();
|
||||
DashScopeChatOptions options = DashScopeChatOptions
|
||||
.builder()
|
||||
.model(DashScopeApi.DEFAULT_CHAT_MODEL)
|
||||
.temperature(0.7)
|
||||
.build();
|
||||
return DashScopeChatModel
|
||||
.builder()
|
||||
DashScopeChatOptions options = DashScopeChatOptions.builder().withModel(DashScopeApi.DEFAULT_CHAT_MODEL)
|
||||
.withTemperature(0.7).build();
|
||||
return DashScopeChatModel.builder()
|
||||
.dashScopeApi(dashScopeApi)
|
||||
.defaultOptions(options)
|
||||
.toolCallingManager(getToolCallingManager())
|
||||
@ -367,7 +368,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* 可参考 QianFanChatAutoConfiguration 的 qianFanChatModel 方法
|
||||
* 可参考 {@link QianFanChatAutoConfiguration} 的 qianFanChatModel 方法
|
||||
*/
|
||||
private static QianFanChatModel buildYiYanChatModel(String key) {
|
||||
// TODO spring ai qianfan 有 bug,无法使用 https://github.com/spring-ai-community/qianfan/issues/6
|
||||
@ -380,7 +381,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* 可参考 QianFanEmbeddingAutoConfiguration 的 qianFanImageModel 方法
|
||||
* 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanImageModel 方法
|
||||
*/
|
||||
private QianFanImageModel buildQianFanImageModel(String key) {
|
||||
// TODO spring ai qianfan 有 bug,无法使用 https://github.com/spring-ai-community/qianfan/issues/6
|
||||
@ -442,7 +443,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
zhiPuAiApiBuilder.baseUrl(url);
|
||||
}
|
||||
ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder().model(ZhiPuAiApi.DEFAULT_CHAT_MODEL).temperature(0.7).build();
|
||||
return new ZhiPuAiChatModel(zhiPuAiApiBuilder.build(), options, getToolCallingManager(), new org.springframework.core.retry.RetryTemplate(),
|
||||
return new ZhiPuAiChatModel(zhiPuAiApiBuilder.build(), options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE,
|
||||
getObservationRegistry().getIfAvailable());
|
||||
}
|
||||
|
||||
@ -462,11 +463,11 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
MiniMaxApi miniMaxApi = StrUtil.isEmpty(url) ? new MiniMaxApi(apiKey)
|
||||
: new MiniMaxApi(url, apiKey);
|
||||
MiniMaxChatOptions options = MiniMaxChatOptions.builder().model(MiniMaxApi.DEFAULT_CHAT_MODEL).temperature(0.7).build();
|
||||
return new MiniMaxChatModel(miniMaxApi, options, getToolCallingManager(), new org.springframework.core.retry.RetryTemplate());
|
||||
return new MiniMaxChatModel(miniMaxApi, options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 可参考 MoonshotChatAutoConfiguration 的 moonshotChatModel 方法
|
||||
* 可参考 {@link MoonshotChatAutoConfiguration} 的 moonshotChatModel 方法
|
||||
*/
|
||||
private MoonshotChatModel buildMoonshotChatModel(String apiKey, String url) {
|
||||
MoonshotApi.Builder moonshotApiBuilder = MoonshotApi.builder()
|
||||
@ -506,8 +507,10 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
* 可参考 {@link OpenAiChatAutoConfiguration} 的 openAiChatModel 方法
|
||||
*/
|
||||
private static OpenAiChatModel buildOpenAiChatModel(String openAiToken, String url) {
|
||||
url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL);
|
||||
OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(url).apiKey(openAiToken).build();
|
||||
return OpenAiChatModel.builder()
|
||||
.openAiClient(buildOpenAiClient(openAiToken, url))
|
||||
.openAiApi(openAiApi)
|
||||
.toolCallingManager(getToolCallingManager())
|
||||
.build();
|
||||
}
|
||||
@ -529,12 +532,13 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
* 可参考 {@link AnthropicChatAutoConfiguration} 的 anthropicApi 方法
|
||||
*/
|
||||
private static AnthropicChatModel buildAnthropicChatModel(String apiKey, String url) {
|
||||
AnthropicOkHttpClient.Builder builder = AnthropicOkHttpClient.builder().apiKey(apiKey);
|
||||
AnthropicApi.Builder builder = AnthropicApi.builder().apiKey(apiKey);
|
||||
if (StrUtil.isNotEmpty(url)) {
|
||||
builder.baseUrl(url);
|
||||
}
|
||||
AnthropicApi anthropicApi = builder.build();
|
||||
return AnthropicChatModel.builder()
|
||||
.anthropicClient(builder.build())
|
||||
.anthropicApi(anthropicApi)
|
||||
.toolCallingManager(getToolCallingManager())
|
||||
.build();
|
||||
}
|
||||
@ -552,7 +556,9 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
* 可参考 {@link OpenAiImageAutoConfiguration} 的 openAiImageModel 方法
|
||||
*/
|
||||
private OpenAiImageModel buildOpenAiImageModel(String openAiToken, String url) {
|
||||
return new OpenAiImageModel(buildOpenAiClient(openAiToken, url));
|
||||
url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL);
|
||||
OpenAiImageApi openAiApi = OpenAiImageApi.builder().baseUrl(url).apiKey(openAiToken).build();
|
||||
return new OpenAiImageModel(openAiApi);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -594,16 +600,16 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
// ========== 各种创建 EmbeddingModel 的方法 ==========
|
||||
|
||||
/**
|
||||
* 可参考 {@link DashScopeEmbeddingAutoConfiguration} 的 DashScopeEmbeddingModel 方法
|
||||
* 可参考 {@link DashScopeEmbeddingAutoConfiguration} 的 dashscopeEmbeddingModel 方法
|
||||
*/
|
||||
private DashScopeEmbeddingModel buildTongYiEmbeddingModel(String apiKey, String model) {
|
||||
DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(apiKey).build();
|
||||
DashScopeEmbeddingOptions dashScopeEmbeddingOptions = DashScopeEmbeddingOptions.builder().model(model).build();
|
||||
DashScopeEmbeddingOptions dashScopeEmbeddingOptions = DashScopeEmbeddingOptions.builder().withModel(model).build();
|
||||
return new DashScopeEmbeddingModel(dashScopeApi, MetadataMode.EMBED, dashScopeEmbeddingOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 可参考 {@link ZhiPuAiEmbeddingAutoConfiguration} 的 ZhiPuAiEmbeddingModel 方法
|
||||
* 可参考 {@link ZhiPuAiEmbeddingAutoConfiguration} 的 zhiPuAiEmbeddingModel 方法
|
||||
*/
|
||||
private ZhiPuAiEmbeddingModel buildZhiPuEmbeddingModel(String apiKey, String url, String model) {
|
||||
ZhiPuAiApi.Builder zhiPuAiApiBuilder = ZhiPuAiApi.builder().apiKey(apiKey);
|
||||
@ -625,7 +631,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* 可参考 {@link QianFanEmbeddingModel} 的 qianFanEmbeddingModel 方法
|
||||
* 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanEmbeddingModel 方法
|
||||
*/
|
||||
private QianFanEmbeddingModel buildYiYanEmbeddingModel(String key, String model) {
|
||||
List<String> keys = StrUtil.split(key, '|');
|
||||
@ -650,16 +656,10 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
* 可参考 {@link OpenAiEmbeddingAutoConfiguration} 的 openAiEmbeddingModel 方法
|
||||
*/
|
||||
private OpenAiEmbeddingModel buildOpenAiEmbeddingModel(String openAiToken, String url, String model) {
|
||||
url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL);
|
||||
OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(url).apiKey(openAiToken).build();
|
||||
OpenAiEmbeddingOptions openAiEmbeddingProperties = OpenAiEmbeddingOptions.builder().model(model).build();
|
||||
return new OpenAiEmbeddingModel(buildOpenAiClient(openAiToken, url), MetadataMode.EMBED, openAiEmbeddingProperties);
|
||||
}
|
||||
|
||||
private static OpenAIClient buildOpenAiClient(String apiKey, String url) {
|
||||
OpenAIOkHttpClient.Builder builder = OpenAIOkHttpClient.builder().apiKey(apiKey);
|
||||
if (StrUtil.isNotEmpty(url)) {
|
||||
builder.baseUrl(url);
|
||||
}
|
||||
return builder.build();
|
||||
return new OpenAiEmbeddingModel(openAiApi, MetadataMode.EMBED, openAiEmbeddingProperties);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -738,7 +738,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
private RedisVectorStore buildRedisVectorStore(EmbeddingModel embeddingModel,
|
||||
Map<String, Class<?>> metadataFields) {
|
||||
// 创建 JedisPooled 对象
|
||||
DataRedisProperties redisProperties = SpringUtils.getBean(DataRedisProperties.class);
|
||||
RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class);
|
||||
JedisPooled jedisPooled = new JedisPooled(redisProperties.getHost(), redisProperties.getPort(),
|
||||
redisProperties.getUsername(), redisProperties.getPassword());
|
||||
// 创建 RedisVectorStoreProperties 对象
|
||||
|
||||
@ -21,14 +21,18 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.springframework.ai.model.ApiKey;
|
||||
import org.springframework.ai.model.NoopApiKey;
|
||||
import org.springframework.ai.model.SimpleApiKey;
|
||||
import org.springframework.ai.openai.api.OpenAiImageApi;
|
||||
import org.springframework.ai.retry.RetryUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.ResponseErrorHandler;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 硅基流动 Image API
|
||||
*
|
||||
@ -54,15 +58,15 @@ public class SiliconFlowImageApi {
|
||||
|
||||
public SiliconFlowImageApi(String baseUrl, String apiKey, RestClient.Builder restClientBuilder,
|
||||
ResponseErrorHandler responseErrorHandler) {
|
||||
this(baseUrl, apiKey, new HttpHeaders(), restClientBuilder, responseErrorHandler);
|
||||
this(baseUrl, apiKey, CollectionUtils.toMultiValueMap(Map.of()), restClientBuilder, responseErrorHandler);
|
||||
}
|
||||
|
||||
public SiliconFlowImageApi(String baseUrl, String apiKey, HttpHeaders headers,
|
||||
public SiliconFlowImageApi(String baseUrl, String apiKey, MultiValueMap<String, String> headers,
|
||||
RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
|
||||
this(baseUrl, new SimpleApiKey(apiKey), headers, restClientBuilder, responseErrorHandler);
|
||||
}
|
||||
|
||||
public SiliconFlowImageApi(String baseUrl, ApiKey apiKey, HttpHeaders headers,
|
||||
public SiliconFlowImageApi(String baseUrl, ApiKey apiKey, MultiValueMap<String, String> headers,
|
||||
RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
|
||||
|
||||
// @formatter:off
|
||||
@ -79,7 +83,7 @@ public class SiliconFlowImageApi {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
public ResponseEntity<SiliconFlowImageResponse> createImage(SiliconflowImageRequest siliconflowImageRequest) {
|
||||
public ResponseEntity<OpenAiImageApi.OpenAiImageResponse> createImage(SiliconflowImageRequest siliconflowImageRequest) {
|
||||
Assert.notNull(siliconflowImageRequest, "Image request cannot be null.");
|
||||
Assert.hasLength(siliconflowImageRequest.prompt(), "Prompt cannot be empty.");
|
||||
|
||||
@ -87,7 +91,7 @@ public class SiliconFlowImageApi {
|
||||
.uri("v1/images/generations")
|
||||
.body(siliconflowImageRequest)
|
||||
.retrieve()
|
||||
.toEntity(SiliconFlowImageResponse.class);
|
||||
.toEntity(OpenAiImageApi.OpenAiImageResponse.class);
|
||||
}
|
||||
|
||||
|
||||
@ -108,15 +112,4 @@ public class SiliconFlowImageApi {
|
||||
}
|
||||
}
|
||||
|
||||
public record SiliconFlowImageResponse(
|
||||
@JsonProperty("created") Long created,
|
||||
@JsonProperty("data") java.util.List<Entry> data) {
|
||||
|
||||
public record Entry(
|
||||
@JsonProperty("url") String url,
|
||||
@JsonProperty("b64_json") String b64Json,
|
||||
@JsonProperty("revised_prompt") String revisedPrompt) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -27,10 +27,11 @@ import org.springframework.ai.image.observation.ImageModelObservationConvention;
|
||||
import org.springframework.ai.image.observation.ImageModelObservationDocumentation;
|
||||
import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.ai.openai.OpenAiImageModel;
|
||||
import org.springframework.ai.openai.api.OpenAiImageApi;
|
||||
import org.springframework.ai.openai.metadata.OpenAiImageGenerationMetadata;
|
||||
import org.springframework.ai.retry.RetryUtils;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@ -70,7 +71,7 @@ public class SiliconFlowImageModel implements ImageModel {
|
||||
|
||||
public SiliconFlowImageModel(SiliconFlowImageApi siliconFlowImageApi, SiliconFlowImageOptions options, RetryTemplate retryTemplate,
|
||||
ObservationRegistry observationRegistry) {
|
||||
Assert.notNull(siliconFlowImageApi, "SiliconFlowImageApi must not be null");
|
||||
Assert.notNull(siliconFlowImageApi, "OpenAiImageApi must not be null");
|
||||
Assert.notNull(options, "options must not be null");
|
||||
Assert.notNull(retryTemplate, "retryTemplate must not be null");
|
||||
Assert.notNull(observationRegistry, "observationRegistry must not be null");
|
||||
@ -95,7 +96,7 @@ public class SiliconFlowImageModel implements ImageModel {
|
||||
.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext,
|
||||
this.observationRegistry)
|
||||
.observe(() -> {
|
||||
ResponseEntity<SiliconFlowImageApi.SiliconFlowImageResponse> imageResponseEntity = this.retryTemplate
|
||||
ResponseEntity<OpenAiImageApi.OpenAiImageResponse> imageResponseEntity = this.retryTemplate
|
||||
.execute(ctx -> this.siliconFlowImageApi.createImage(imageRequest));
|
||||
|
||||
ImageResponse imageResponse = convertResponse(imageResponseEntity, imageRequest);
|
||||
@ -108,22 +109,17 @@ public class SiliconFlowImageModel implements ImageModel {
|
||||
|
||||
private SiliconFlowImageApi.SiliconflowImageRequest createRequest(ImagePrompt imagePrompt,
|
||||
SiliconFlowImageOptions requestImageOptions) {
|
||||
String instructions = imagePrompt.getInstructions().getFirst().getText();
|
||||
String instructions = imagePrompt.getInstructions().get(0).getText();
|
||||
|
||||
return new SiliconFlowImageApi.SiliconflowImageRequest(
|
||||
instructions,
|
||||
ModelOptionsUtils.mergeOption(requestImageOptions.getModel(), SiliconFlowApiConstants.DEFAULT_IMAGE_MODEL),
|
||||
requestImageOptions.getN(),
|
||||
requestImageOptions.getNegativePrompt(),
|
||||
requestImageOptions.getSeed(),
|
||||
requestImageOptions.getNumInferenceSteps(),
|
||||
requestImageOptions.getGuidanceScale(),
|
||||
requestImageOptions.getImage());
|
||||
SiliconFlowImageApi.SiliconflowImageRequest imageRequest = new SiliconFlowImageApi.SiliconflowImageRequest(instructions,
|
||||
SiliconFlowApiConstants.DEFAULT_IMAGE_MODEL);
|
||||
|
||||
return ModelOptionsUtils.merge(requestImageOptions, imageRequest, SiliconFlowImageApi.SiliconflowImageRequest.class);
|
||||
}
|
||||
|
||||
private ImageResponse convertResponse(ResponseEntity<SiliconFlowImageApi.SiliconFlowImageResponse> imageResponseEntity,
|
||||
private ImageResponse convertResponse(ResponseEntity<OpenAiImageApi.OpenAiImageResponse> imageResponseEntity,
|
||||
SiliconFlowImageApi.SiliconflowImageRequest siliconflowImageRequest) {
|
||||
SiliconFlowImageApi.SiliconFlowImageResponse imageApiResponse = imageResponseEntity.getBody();
|
||||
OpenAiImageApi.OpenAiImageResponse imageApiResponse = imageResponseEntity.getBody();
|
||||
if (imageApiResponse == null) {
|
||||
logger.warn("No image response returned for request: {}", siliconflowImageRequest);
|
||||
return new ImageResponse(List.of());
|
||||
@ -140,17 +136,12 @@ public class SiliconFlowImageModel implements ImageModel {
|
||||
}
|
||||
|
||||
private SiliconFlowImageOptions mergeOptions(@Nullable ImageOptions runtimeOptions, SiliconFlowImageOptions defaultOptions) {
|
||||
if (runtimeOptions == null) {
|
||||
var runtimeOptionsForProvider = ModelOptionsUtils.copyToTarget(runtimeOptions, ImageOptions.class,
|
||||
SiliconFlowImageOptions.class);
|
||||
|
||||
if (runtimeOptionsForProvider == null) {
|
||||
return defaultOptions;
|
||||
}
|
||||
SiliconFlowImageOptions runtimeOptionsForProvider = runtimeOptions instanceof SiliconFlowImageOptions siliconFlowImageOptions
|
||||
? siliconFlowImageOptions
|
||||
: SiliconFlowImageOptions.builder()
|
||||
.model(runtimeOptions.getModel())
|
||||
.batchSize(runtimeOptions.getN())
|
||||
.width(runtimeOptions.getWidth())
|
||||
.height(runtimeOptions.getHeight())
|
||||
.build();
|
||||
|
||||
return SiliconFlowImageOptions.builder()
|
||||
// Handle portable image options
|
||||
|
||||
@ -1,25 +1,26 @@
|
||||
package cn.iocoder.yudao.module.ai.framework.security.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import jakarta.annotation.Resource;
|
||||
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;
|
||||
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* AI 模块的 Security 配置
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false, value = "aiSecurityConfiguration")
|
||||
public class SecurityConfiguration {
|
||||
|
||||
@Value("${spring.ai.mcp.server.sse-endpoint:/sse}")
|
||||
private String mcpSseEndpoint;
|
||||
@Value("${spring.ai.mcp.server.sse-message-endpoint:/mcp/message}")
|
||||
private String mcpSseMessageEndpoint;
|
||||
@Value("${spring.ai.mcp.server.streamable-http-endpoint:/mcp}")
|
||||
private String mcpStreamableHttpEndpoint;
|
||||
@Resource
|
||||
private Optional<McpServerSseProperties> mcpServerSseProperties;
|
||||
@Resource
|
||||
private Optional<McpServerStreamableHttpProperties> mcpServerStreamableHttpProperties;
|
||||
|
||||
@Bean("aiAuthorizeRequestsCustomizer")
|
||||
public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() {
|
||||
@ -27,15 +28,12 @@ public class SecurityConfiguration {
|
||||
|
||||
@Override
|
||||
public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
|
||||
if (StrUtil.isNotBlank(mcpSseEndpoint)) {
|
||||
registry.requestMatchers(mcpSseEndpoint).permitAll();
|
||||
}
|
||||
if (StrUtil.isNotBlank(mcpSseMessageEndpoint)) {
|
||||
registry.requestMatchers(mcpSseMessageEndpoint).permitAll();
|
||||
}
|
||||
if (StrUtil.isNotBlank(mcpStreamableHttpEndpoint)) {
|
||||
registry.requestMatchers(mcpStreamableHttpEndpoint).permitAll();
|
||||
}
|
||||
mcpServerSseProperties.ifPresent(properties -> {
|
||||
registry.requestMatchers(properties.getSseEndpoint()).permitAll();
|
||||
registry.requestMatchers(properties.getSseMessageEndpoint()).permitAll();
|
||||
});
|
||||
mcpServerStreamableHttpProperties.ifPresent(properties ->
|
||||
registry.requestMatchers(properties.getMcpEndpoint()).permitAll());
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@ -49,10 +49,10 @@ 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.common.autoconfigure.properties.McpClientCommonProperties;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.resolution.ToolCallbackResolver;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import reactor.core.publisher.Flux;
|
||||
@ -130,8 +130,9 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
||||
@Autowired(required = false) // 由于 yudao.ai.mcp.client.enable 配置项,可以关闭 McpSyncClient 的功能,所以这里只能不强制注入
|
||||
private List<McpSyncClient> mcpClients;
|
||||
|
||||
@Value("${spring.ai.mcp.client.name:mcp}")
|
||||
private String mcpClientName;
|
||||
@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
|
||||
@Autowired(required = false) // 由于 yudao.ai.mcp.client.enable 配置项,可以关闭 McpSyncClient 的功能,所以这里只能不强制注入
|
||||
private McpClientCommonProperties mcpClientCommonProperties;
|
||||
|
||||
@Resource
|
||||
private ToolCallbackResolver toolCallbackResolver;
|
||||
@ -409,16 +410,13 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
||||
if (CollUtil.isNotEmpty(mcpClients) && CollUtil.isNotEmpty(chatRole.getMcpClientNames())) {
|
||||
chatRole.getMcpClientNames().forEach(mcpClientName -> {
|
||||
// 2.1 标准化名字,参考 McpClientAutoConfiguration 的 connectedClientName 方法
|
||||
String finalMcpClientName = this.mcpClientName + " - " + mcpClientName;
|
||||
String finalMcpClientName = mcpClientCommonProperties.getName() + " - " + mcpClientName;
|
||||
// 2.2 匹配对应的 McpSyncClient
|
||||
mcpClients.forEach(mcpClient -> {
|
||||
if (ObjUtil.notEqual(mcpClient.getClientInfo().name(), finalMcpClientName)) {
|
||||
return;
|
||||
}
|
||||
ToolCallback[] mcpToolCallBacks = SyncMcpToolCallbackProvider.builder()
|
||||
.mcpClients(mcpClient)
|
||||
.build()
|
||||
.getToolCallbacks();
|
||||
ToolCallback[] mcpToolCallBacks = new SyncMcpToolCallbackProvider(mcpClient).getToolCallbacks();
|
||||
CollUtil.addAll(toolCallbacks, mcpToolCallBacks);
|
||||
});
|
||||
});
|
||||
@ -541,7 +539,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
||||
public void deleteChatMessageByConversationId(Long conversationId, Long userId) {
|
||||
// 1. 校验消息存在
|
||||
List<AiChatMessageDO> messages = chatMessageMapper.selectListByConversationId(conversationId);
|
||||
if (CollUtil.isEmpty(messages) || ObjUtil.notEqual(messages.getFirst().getUserId(), userId)) {
|
||||
if (CollUtil.isEmpty(messages) || ObjUtil.notEqual(messages.get(0).getUserId(), userId)) {
|
||||
throw exception(CHAT_MESSAGE_NOT_EXIST);
|
||||
}
|
||||
// 2. 执行删除
|
||||
|
||||
@ -166,7 +166,7 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
|
||||
segmentMapper.deleteByIds(convertList(segments, AiKnowledgeSegmentDO::getId));
|
||||
|
||||
// 3. 删除向量存储中的段落
|
||||
VectorStore vectorStore = getVectorStoreById(segments.getFirst().getKnowledgeId());
|
||||
VectorStore vectorStore = getVectorStoreById(segments.get(0).getKnowledgeId());
|
||||
vectorStore.delete(convertList(segments, AiKnowledgeSegmentDO::getVectorId));
|
||||
}
|
||||
|
||||
@ -299,7 +299,7 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
|
||||
// 2. Rerank 重排序
|
||||
if (rerankModel != null) {
|
||||
RerankResponse rerankResponse = rerankModel.call(new RerankRequest(reqBO.getContent(), documents,
|
||||
DashScopeRerankOptions.builder().topN(topK).build()));
|
||||
DashScopeRerankOptions.builder().withTopN(topK).build()));
|
||||
documents = convertList(rerankResponse.getResults(),
|
||||
documentWithScore -> documentWithScore.getScore() >= similarityThreshold
|
||||
? documentWithScore.getOutput() : null);
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
|
||||
|
||||
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ai.anthropic.AnthropicChatModel;
|
||||
import org.springframework.ai.anthropic.AnthropicChatOptions;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.SystemMessage;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
@ -23,12 +23,12 @@ import java.util.List;
|
||||
public class AnthropicChatModelTest {
|
||||
|
||||
private final AnthropicChatModel chatModel = AnthropicChatModel.builder()
|
||||
.anthropicClient(AnthropicOkHttpClient.builder()
|
||||
.anthropicApi(AnthropicApi.builder()
|
||||
.apiKey("sk-muubv7cXeLw0Etgs743f365cD5Ea44429946Fa7e672d8942")
|
||||
.baseUrl("https://aihubmix.com")
|
||||
.build())
|
||||
.options(AnthropicChatOptions.builder()
|
||||
.model("claude-sonnet-4-5")
|
||||
.defaultOptions(AnthropicChatOptions.builder()
|
||||
.model(AnthropicApi.ChatModel.CLAUDE_SONNET_4_5)
|
||||
.temperature(0.7)
|
||||
.maxTokens(4096)
|
||||
.build())
|
||||
@ -70,7 +70,8 @@ public class AnthropicChatModelTest {
|
||||
List<Message> messages = new ArrayList<>();
|
||||
messages.add(new UserMessage("thkinking 下,1+1 为什么等于 2 "));
|
||||
AnthropicChatOptions options = AnthropicChatOptions.builder()
|
||||
.model("claude-sonnet-4-5")
|
||||
.model(AnthropicApi.ChatModel.CLAUDE_SONNET_4_5)
|
||||
.thinking(AnthropicApi.ThinkingType.ENABLED, 3096)
|
||||
.temperature(1D)
|
||||
.build();
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
|
||||
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel;
|
||||
import com.openai.client.okhttp.OpenAIOkHttpClient;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
@ -11,6 +10,7 @@ import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -24,11 +24,11 @@ import java.util.List;
|
||||
public class BaiChuanChatModelTests {
|
||||
|
||||
private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
|
||||
.openAiClient(OpenAIOkHttpClient.builder()
|
||||
.openAiApi(OpenAiApi.builder()
|
||||
.baseUrl(BaiChuanChatModel.BASE_URL)
|
||||
.apiKey("sk-61b6766a94c70786ed02673f5e16af3c") // apiKey
|
||||
.build())
|
||||
.options(OpenAiChatOptions.builder()
|
||||
.defaultOptions(OpenAiChatOptions.builder()
|
||||
.model("Baichuan4-Turbo") // 模型(https://platform.baichuan-ai.com/docs/api)
|
||||
.temperature(0.7)
|
||||
.build())
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user