30 Achegas 70b94daf7f ... 34dd258c8f

Autor SHA1 Mensaxe Data
  bwcx_jzy 34dd258c8f commit beta 2.11.12.1 hai 2 meses
  bwcx_jzy 3a9eec3622 refactor: 移除国际站链接 hai 2 meses
  bwcx_jzy 28c1c3b28e releases(发布): 准备 2.11.12.1 hai 2 meses
  bwcx_jzy eabdf7795e feat(server): 优化 Docker镜像构建支持配置网络模式- 在 Docker 镜像构建过程中添加网络模式配置选项 hai 2 meses
  bwcx_jzy 36d1358aa2 Merge branch 'refs/heads/dev-ftp1' into dev hai 3 meses
  bwcx_jzy cd01f8c9d1 fix(web-vue): 修复表格自定义列在部分字段不生效问题 hai 3 meses
  wxyshine f5e69e07d0 feat(server): 国际化替换: 新增构建后发布到ftp hai 3 meses
  wxyshine ce3200b1db feat(server): 新增构建后发布到ftp hai 3 meses
  bwcx_jzy 2e8a9e8c31 feat(server): 优化脚本日志和 SSH 命令日志支持批量删除 hai 4 meses
  bwcx_jzy d053fa5449 fix(server): 修复终端输入命令时按 Backspace 会退出终端的问题 hai 4 meses
  wxyshine 8abd4d42b0 feat(server): 国际化替换:完善功能管理FTP列表功能,系统管理FTP列表功能 hai 4 meses
  bwcx_jzy bda44b28b3 refactor(server): 抽离控制台按键事件处理逻辑 hai 4 meses
  wxyshine 9a709923cd feat(server): 完善功能管理FTP列表功能,系统管理FTP列表功能 hai 4 meses
  bwcx_jzy 3fe63c1db2 docs: 更新 CHANGELOG-BETA.md 文件 hai 5 meses
  bwcx_jzy 4d82edc6b7 feat(server): 添加对达梦数据库的支持 hai 5 meses
  bwcx_jzy ec731d5b3e feat(server): 新增达梦数据库支持 hai 5 meses
  wxyshine b5eb936be5 feat(storage): 适配达梦数据库 hai 5 meses
  bwcx_jzy e8b3079b09 refactor(system): 优化工作空间数据修复和初始化事件 hai 5 meses
  bwcx_jzy a074f9a1f5 Merge branch 'refs/heads/dev' into dameng hai 5 meses
  bwcx_jzy 1e23960c58 refactor(h2db): 优化 SQL 查询语句 hai 5 meses
  bwcx_jzy ebf40ead28 refactor(system): 更新工作空间绑定集群 ID 的方式 hai 5 meses
  wxyshine 396ddc7d01 feat(): 适配达梦数据库 hai 5 meses
  wxyshine 19042cc134 feat(): 适配达梦数据库 hai 5 meses
  bwcx_jzy 4dcc00e1d6 docs(jpom-parent): 更新 PLANS.md 文件 hai 5 meses
  bwcx_jzy 15242c534a feat(server): 添加数据库表名前缀支持 hai 5 meses
  bwcx_jzy 0488d6af38 feat(storage): 添加达梦数据库支持 hai 5 meses
  bwcx_jzy 9ed2761191 feat(assets): 添加 FTP资产管理功能 hai 7 meses
  bwcx_jzy a2ffedee39 Merge remote-tracking branch 'origin/dev' into dev hai 9 meses
  bwcx_jzy bfc7f2bcb5 feat(server): 新增 FTP资产管理功能 hai 9 meses
  bwcx_jzy 678064a8fe refactor(server): 优化数据库操作日志信息 hai 9 meses
Modificáronse 100 ficheiros con 4027 adicións e 468 borrados
  1. 17 0
      CHANGELOG-BETA.md
  2. 8 6
      PLANS.md
  3. 0 3
      README-en.md
  4. 0 4
      README.md
  5. 1 1
      env-beta.env
  6. 1 1
      modules/agent-transport/agent-transport-common/pom.xml
  7. 1 1
      modules/agent-transport/agent-transport-http/pom.xml
  8. 2 2
      modules/agent-transport/pom.xml
  9. 1 1
      modules/agent/DockerfileBeta
  10. 2 2
      modules/agent/pom.xml
  11. 2 2
      modules/common/pom.xml
  12. 1 1
      modules/common/src/main/resources/banner.txt
  13. 47 2
      modules/common/src/main/resources/i18n/messages_en_US.properties
  14. 51 6
      modules/common/src/main/resources/i18n/messages_zh_CN.properties
  15. 47 2
      modules/common/src/main/resources/i18n/messages_zh_HK.properties
  16. 47 2
      modules/common/src/main/resources/i18n/messages_zh_TW.properties
  17. 45 0
      modules/common/src/main/resources/i18n/words.json
  18. 4 5
      modules/common/src/test/resources/baidubce_translate.txt
  19. 1 1
      modules/server/DockerfileBeta
  20. 1 1
      modules/server/DockerfileBetaJdk17
  21. 14 2
      modules/server/pom.xml
  22. 2 0
      modules/server/src/main/java/org/dromara/jpom/JpomServerApplication.java
  23. 8 0
      modules/server/src/main/java/org/dromara/jpom/build/BuildExtraModule.java
  24. 82 11
      modules/server/src/main/java/org/dromara/jpom/build/ReleaseManage.java
  25. 29 0
      modules/server/src/main/java/org/dromara/jpom/configuration/AssetsConfig.java
  26. 5 1
      modules/server/src/main/java/org/dromara/jpom/controller/DataStatController.java
  27. 44 1
      modules/server/src/main/java/org/dromara/jpom/controller/build/BuildInfoController.java
  28. 211 0
      modules/server/src/main/java/org/dromara/jpom/controller/ftp/FtpController.java
  29. 66 0
      modules/server/src/main/java/org/dromara/jpom/controller/ftp/FtpFileController.java
  30. 34 9
      modules/server/src/main/java/org/dromara/jpom/controller/script/ScriptLogController.java
  31. 12 9
      modules/server/src/main/java/org/dromara/jpom/controller/ssh/CommandLogController.java
  32. 3 3
      modules/server/src/main/java/org/dromara/jpom/controller/system/BackupInfoController.java
  33. 1 1
      modules/server/src/main/java/org/dromara/jpom/controller/system/SystemConfigController.java
  34. 11 7
      modules/server/src/main/java/org/dromara/jpom/controller/system/WorkspaceController.java
  35. 635 0
      modules/server/src/main/java/org/dromara/jpom/func/assets/controller/BaseFtpFileController.java
  36. 457 0
      modules/server/src/main/java/org/dromara/jpom/func/assets/controller/MachineFtpController.java
  37. 75 0
      modules/server/src/main/java/org/dromara/jpom/func/assets/controller/MachineFtpFileController.java
  38. 1 1
      modules/server/src/main/java/org/dromara/jpom/func/assets/controller/MachineSshController.java
  39. 115 0
      modules/server/src/main/java/org/dromara/jpom/func/assets/model/MachineFtpModel.java
  40. 245 0
      modules/server/src/main/java/org/dromara/jpom/func/assets/server/MachineFtpServer.java
  41. 1 1
      modules/server/src/main/java/org/dromara/jpom/func/assets/server/MachineNodeStatLogServer.java
  42. 6 2
      modules/server/src/main/java/org/dromara/jpom/func/system/service/ClusterInfoService.java
  43. 125 0
      modules/server/src/main/java/org/dromara/jpom/model/data/FtpModel.java
  44. 1 0
      modules/server/src/main/java/org/dromara/jpom/model/enums/BuildReleaseMethod.java
  45. 15 3
      modules/server/src/main/java/org/dromara/jpom/permission/ClassFeature.java
  46. 3 3
      modules/server/src/main/java/org/dromara/jpom/service/dblog/BackupInfoService.java
  47. 33 5
      modules/server/src/main/java/org/dromara/jpom/service/dblog/BuildInfoService.java
  48. 2 1
      modules/server/src/main/java/org/dromara/jpom/service/docker/DockerInfoService.java
  49. 3 4
      modules/server/src/main/java/org/dromara/jpom/service/h2db/BaseDbService.java
  50. 255 0
      modules/server/src/main/java/org/dromara/jpom/service/node/ftp/FtpService.java
  51. 5 1
      modules/server/src/main/java/org/dromara/jpom/service/node/script/NodeScriptServer.java
  52. 4 1
      modules/server/src/main/java/org/dromara/jpom/service/node/ssh/SshCommandService.java
  53. 8 2
      modules/server/src/main/java/org/dromara/jpom/service/outgiving/OutGivingServer.java
  54. 6 1
      modules/server/src/main/java/org/dromara/jpom/service/script/ScriptServer.java
  55. 10 2
      modules/server/src/main/java/org/dromara/jpom/service/system/WorkspaceService.java
  56. 3 2
      modules/server/src/main/java/org/dromara/jpom/service/user/TriggerTokenLogServer.java
  57. 4 2
      modules/server/src/main/java/org/dromara/jpom/service/user/UserService.java
  58. 46 0
      modules/server/src/main/java/org/dromara/jpom/socket/handler/KeyControl.java
  59. 202 0
      modules/server/src/main/java/org/dromara/jpom/socket/handler/KeyEventCycle.java
  60. 5 231
      modules/server/src/main/java/org/dromara/jpom/socket/handler/SshHandler.java
  61. 7 3
      modules/server/src/main/java/org/dromara/jpom/system/db/DataInitEvent.java
  62. 37 20
      modules/server/src/main/java/org/dromara/jpom/system/db/InitDb.java
  63. 17 0
      modules/server/src/main/resources/application-dameng.yml
  64. 1 1
      modules/server/src/main/resources/application-mariadb.yml
  65. 1 1
      modules/server/src/main/resources/application-mysql.yml
  66. 1 1
      modules/server/src/main/resources/application-postgresql.yml
  67. 16 6
      modules/server/src/main/resources/application.yml
  68. 10 1
      modules/server/src/main/resources/config_default/application.yml
  69. 11 0
      modules/server/src/main/resources/menus/zh-CN/index.json
  70. 5 0
      modules/server/src/main/resources/menus/zh-CN/system.json
  71. 2 0
      modules/server/src/main/resources/sql-view/table.all.v1.1.csv
  72. 39 0
      modules/server/src/main/resources/sql-view/table.all.v1.2.csv
  73. 3 3
      modules/server/src/test/java/org/dromara/jpom/H2TableToCsv2Test.java
  74. 2 2
      modules/server/src/test/java/org/dromara/jpom/service/h2db/H2ToolTest.java
  75. 3 2
      modules/storage-module/pom.xml
  76. 1 1
      modules/storage-module/storage-module-common/pom.xml
  77. 11 4
      modules/storage-module/storage-module-common/src/main/java/org/dromara/jpom/db/BaseDbCommonService.java
  78. 12 2
      modules/storage-module/storage-module-common/src/main/java/org/dromara/jpom/db/DbExtConfig.java
  79. 64 51
      modules/storage-module/storage-module-common/src/main/java/org/dromara/jpom/db/StorageServiceFactory.java
  80. 13 2
      modules/storage-module/storage-module-common/src/main/java/org/dromara/jpom/db/StorageTableFactory.java
  81. 18 1
      modules/storage-module/storage-module-common/src/main/java/org/dromara/jpom/db/TableViewRowData.java
  82. 61 3
      modules/storage-module/storage-module-common/src/main/java/org/dromara/jpom/dialect/DialectUtil.java
  83. 9 0
      modules/storage-module/storage-module-dameng/README.md
  84. 53 0
      modules/storage-module/storage-module-dameng/pom.xml
  85. 96 0
      modules/storage-module/storage-module-dameng/src/main/java/org/dromara/jpom/storage/DamengStorageServiceImpl.java
  86. 298 0
      modules/storage-module/storage-module-dameng/src/main/java/org/dromara/jpom/storage/DamengTableBuilderImpl.java
  87. 1 0
      modules/storage-module/storage-module-dameng/src/main/resources/META-INF/services/org.dromara.jpom.db.IStorageService
  88. 1 0
      modules/storage-module/storage-module-dameng/src/main/resources/META-INF/services/org.dromara.jpom.db.IStorageSqlBuilderService
  89. 149 0
      modules/storage-module/storage-module-dameng/src/main/resources/sql-view/execute.dameng.v1.0.sql
  90. 1 1
      modules/storage-module/storage-module-h2/pom.xml
  91. 1 1
      modules/storage-module/storage-module-h2/src/main/java/org/dromara/jpom/plugin/DefaultDbH2PluginImpl.java
  92. 6 6
      modules/storage-module/storage-module-h2/src/main/java/org/dromara/jpom/storage/H2StorageServiceImpl.java
  93. 1 1
      modules/storage-module/storage-module-mariadb/pom.xml
  94. 1 1
      modules/storage-module/storage-module-mysql/pom.xml
  95. 1 1
      modules/storage-module/storage-module-postgresql/pom.xml
  96. 1 1
      modules/storage-module/storage-module-postgresql/src/main/java/org/dromara/jpom/storage/PostgresqlTableBuilderImpl.java
  97. 1 1
      modules/sub-plugin/docker-cli/pom.xml
  98. 4 0
      modules/sub-plugin/docker-cli/src/main/java/org/dromara/jpom/DefaultDockerPluginImpl.java
  99. 1 1
      modules/sub-plugin/email/pom.xml
  100. 1 1
      modules/sub-plugin/encrypt/pom.xml

+ 17 - 0
CHANGELOG-BETA.md

@@ -1,5 +1,22 @@
 # 🚀 版本日志
 
+## 2.11.12.1-beta (2025-07-30)
+
+### 🐣 新增功能
+
+1. 【server】新增 数据库支持使用达梦数据库(感谢[@wxyShine](https://gitee.com/wxyShine) )
+2. 【server】新增 FTP 管理,文件查看和构建发布方式新增 FTP 支持
+
+### 🐞 解决BUG、优化功能
+
+1. 【server】优化 数据库表支持配置前缀 `jpom.db.table-prefix` (感谢@ccx2480)
+2. 【server】修复 终端输入命令,按Backspace 会退出终端(感谢[@dgs](https://gitee.com/dgs0924) [Gitee issues ICA57K](https://gitee.com/dromara/Jpom/issues/ICA57K) )
+3. 【server】优化 脚本日志和 SSH 命令日志支持批量删除(感谢[@lin_yeqi](https://gitee.com/lin_yeqi) [Gitee issues IBIM6W](https://gitee.com/dromara/Jpom/issues/IBIM6W) )
+4. 【server】修复 表格自定义列在部分字段不生效情况
+5. 【server】优化 构建发布方式中 Docker 镜像支持配置网络模式(感谢@酱总)
+
+------
+
 ## 2.11.11.4-beta (2025-05-06)
 
 ### 🐣 新增功能

+ 8 - 6
PLANS.md

@@ -5,21 +5,23 @@
 1. **构建流水线**
 2. **netty-agent**
 3. 凭证管理
-4. 升级 JDK 11 或者 17
+4. !升级 JDK 11 或者 17
 5. 端口监控、监控报警、机器监控、ssh 监控报警
 6. 资产监控
-7. nginx 流量切换(nginx 功能可能下线)
+7. ~~nginx 流量切换(nginx 功能可能下线)~~
 8. acme.sh ui
 9. 执行审计
 10. 执行部分命令耗时和直接执行相差太大
 11. **非 root 用户提升权限写入 root 用户文件**
-12. 部分数据迁移工作空间(~~项目~~,构建,仓库、节点分发)
-13. 前端表格用户自定义列显示
-14. 节点取消,白名单配置和下载白名单(统一到服务端工作空间配置)
+12. !部分数据迁移工作空间(~~项目~~,构建,仓库、节点分发)
+13. ~~前端表格用户自定义列显示~~s
+14. !节点取消,白名单配置和下载白名单(统一到服务端工作空间配置)
 15. 隧道节点
 16. docker-compose       sh
 17. 监控通知模块优化支持更多(飞书)    zx
-18. 数据库支持 mariadb
+18. ~~数据库支持 mariadb~~
+19. SSH 回溯
+20. FTP
 
 ## 2.10.x
 

+ 0 - 3
README-en.md

@@ -37,9 +37,6 @@
     </a>
 </p>
 
-<p align="center">
-	👉 International Station:<a target="_blank" href="https://jpom.dromara.org">https://jpom.dromara.org</a>  👈
-</p>
 <p align="center">
 	👉Mainland Station:<a target="_blank" href="https://jpom.top">https://jpom.top/</a>  👈
 </p>

+ 0 - 4
README.md

@@ -40,10 +40,6 @@
     </a>
 </p>
 
-<p align="center">
-	👉 国际站:<a target="_blank" href="https://jpom.dromara.org">https://jpom.dromara.org</a>  👈
-</p>
-
 <p align="center">
   👉 大陆站:<a target="_blank" href="https://jpom.top">https://jpom.top</a>  👈
 </p>

+ 1 - 1
env-beta.env

@@ -1,3 +1,3 @@
-JPOM_VERSION=2.11.11.4
+JPOM_VERSION=2.11.12.1
 # Server Token 生产部署请更换
 SERVER_TOKEN=7094f673-2c53-4fc1-82e7-86e528449d97

+ 1 - 1
modules/agent-transport/agent-transport-common/pom.xml

@@ -17,7 +17,7 @@
     <parent>
         <groupId>org.dromara.jpom.agent-transport</groupId>
         <artifactId>jpom-agent-transport-parent</artifactId>
-        <version>2.11.12</version>
+        <version>2.11.12.1</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
 

+ 1 - 1
modules/agent-transport/agent-transport-http/pom.xml

@@ -17,7 +17,7 @@
     <parent>
         <groupId>org.dromara.jpom.agent-transport</groupId>
         <artifactId>jpom-agent-transport-parent</artifactId>
-        <version>2.11.12</version>
+        <version>2.11.12.1</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
 

+ 2 - 2
modules/agent-transport/pom.xml

@@ -16,7 +16,7 @@
     <parent>
         <artifactId>jpom-parent</artifactId>
         <groupId>org.dromara.jpom</groupId>
-        <version>2.11.12</version>
+        <version>2.11.12.1</version>
         <relativePath>../../pom.xml</relativePath>
     </parent>
     <packaging>pom</packaging>
@@ -25,7 +25,7 @@
         <module>agent-transport-http</module>
     </modules>
     <modelVersion>4.0.0</modelVersion>
-    <version>2.11.12</version>
+    <version>2.11.12.1</version>
     <groupId>org.dromara.jpom.agent-transport</groupId>
     <artifactId>jpom-agent-transport-parent</artifactId>
     <name>Jpom Agent Transport</name>

+ 1 - 1
modules/agent/DockerfileBeta

@@ -16,7 +16,7 @@ LABEL maintainer="bwcx-jzy <bwcx_jzy@163.com>"
 LABEL documentation="https://jpom.top"
 
 ENV JPOM_HOME=/usr/local/jpom-agent
-ENV JPOM_PKG_VERSION=2.11.11.4
+ENV JPOM_PKG_VERSION=2.11.12.1
 ENV JPOM_PKG=agent-${JPOM_PKG_VERSION}-release.tar.gz
 ENV SHA1_NAME=agent-${JPOM_PKG_VERSION}-release.tar.gz.sha1
 

+ 2 - 2
modules/agent/pom.xml

@@ -16,12 +16,12 @@
     <parent>
         <artifactId>jpom-parent</artifactId>
         <groupId>org.dromara.jpom</groupId>
-        <version>2.11.12</version>
+        <version>2.11.12.1</version>
         <relativePath>../../pom.xml</relativePath>
     </parent>
     <modelVersion>4.0.0</modelVersion>
     <artifactId>agent</artifactId>
-    <version>2.11.12</version>
+    <version>2.11.12.1</version>
     <name>Jpom Agent</name>
     <properties>
         <start-class>org.dromara.jpom.JpomAgentApplication</start-class>

+ 2 - 2
modules/common/pom.xml

@@ -16,13 +16,13 @@
     <parent>
         <artifactId>jpom-parent</artifactId>
         <groupId>org.dromara.jpom</groupId>
-        <version>2.11.12</version>
+        <version>2.11.12.1</version>
         <relativePath>../../pom.xml</relativePath>
     </parent>
     <modelVersion>4.0.0</modelVersion>
     <name>Jpom Common</name>
     <artifactId>common</artifactId>
-    <version>2.11.12</version>
+    <version>2.11.12.1</version>
 
     <dependencies>
 

+ 1 - 1
modules/common/src/main/resources/banner.txt

@@ -7,4 +7,4 @@
         | |
         |_|
  ➜ Jpom is licensed under Mulan PSL v2.
- ➜ \ (•◡•) / (v2.11.12)
+ ➜ \ (•◡•) / (v2.11.12.1)

+ 47 - 2
modules/common/src/main/resources/i18n/messages_en_US.properties

@@ -1,5 +1,5 @@
 #i18n en_US
-#Thu Jan 09 16:59:46 CST 2025
+#Thu Jun 12 16:41:19 CST 2025
 i18n.ssh_info_does_not_exist.5ed0=SSH information does not exist
 i18n.incompatible_program_versions.5291=The current program version {} The new version of the program is minimum compatible {} and cannot be upgraded directly
 i18n.no_projects_configured.e873=No items are configured
@@ -8,6 +8,7 @@ i18n.machine_ssh_info.8dbb=Machine SSH information
 i18n.machine_info_not_exist.3468=The corresponding machine information does not exist.
 i18n.unsupported_method.a1de=Unsupported way
 i18n.delete_local_image_failed.91fa=Failed to delete local image
+i18n.cluster_not_grouped.8f54=The current cluster has not bound packets and cannot monitor FTP asset information
 i18n.service_name_in_cluster_required.5446=Please fill in the service name in the cluster
 i18n.cluster_manager_node_not_found.1cd0=No cluster management node found
 i18n.distribute_id_already_exists.2168=The distribution id already exists
@@ -34,6 +35,7 @@ i18n.start_waiting_for_data_migration.e76f=Start waiting for data migration
 i18n.empty_file_or_folder_for_publish.cae8=The published file or folder is empty and cannot continue publishing
 i18n.demo_account_cannot_use_feature.a1a1=The demo account cannot use this function.
 i18n.repository_type_required.9414=Please select a warehouse type
+i18n.start_backup_database.e554=Start backing up the database
 i18n.async_resource_expired.2ddc=The asynchronous resource has expired and needs to be actively closed, {} {}
 i18n.mark_cannot_be_empty.1927=Tag cannot be empty
 i18n.get_success.fb55=Get success
@@ -50,9 +52,11 @@ i18n.upload_failed_no_matching_project.b219=Upload failed, no corresponding dist
 i18n.no_branches_or_tags_in_repository.76b6=The warehouse does not have any branches or labels.
 i18n.download_file_description.10cb=Download file {} {} {}
 i18n.delay_build.7d62=Start building after a delay of {} seconds
+i18n.ftp_not_exist.f9b3=There is no corresponding ftp.
 i18n.default_cluster.38cf=default cluster
 i18n.node_and_check_project_failed.ac4b=Node and check item failed
 i18n.load_oauth2_config.da42=Load oauth2 configuration :{} {}
+i18n.affected_rows.5781=Number of rows affected
 i18n.cluster_not_bound_to_group_for_node_monitoring.1586=The current cluster has not been bound to a group, so the asset information of the cluster nodes cannot be monitored
 i18n.cannot_execute_error.4c29=Cannot execute\: error
 i18n.no_environment_variable.c79f=No corresponding environment variables
@@ -114,6 +118,7 @@ i18n.no_ssh_entry_found.d0e1=No corresponding ssh entry found\: {}
 i18n.build_runs_on_image_interrupted.00fd=Building the runsOn image was interrupted
 i18n.incorrect_parameter_format.9efb=The format of the passed parameter is incorrect
 i18n.multiple_certificate_files_found.bee3=Found more than 2 certificate files
+i18n.file_not_exist_or_unable_to_download.b977=The file does not exist or cannot be downloaded.
 i18n.invalid_runs_on_image_name.4b96=runsOn image name is invalid
 i18n.dockerfile_path_required.69ac=Please fill in the path to the Dockerfile to be executed
 i18n.file_upload_mode_not_configured.b3b2=No profile upload mode
@@ -248,6 +253,7 @@ i18n.select_node_to_modify.6617=Please select the section to modify
 i18n.correct_dingtalk_address_required.2b4a=Please enter the correct DingTalk address
 i18n.query_folder_sftp_failed.9d35=Failed to query folder SFTP,
 i18n.name_field_required.e0c5=Line {} name field cannot be empty
+i18n.ftp_connection_failed_message.bd99=Connection to FTP failed\:
 i18n.plugin_end_log_connection_successful.9035=Connection successful\: plugin-side log
 i18n.start_building_with_number_and_path.c41c=Start building \#{} Build execution path\: {}
 i18n.start_executing_upload_pre_command.fb5c=Start executing the pre-upload command
@@ -292,6 +298,7 @@ i18n.login_name_cannot_contain_chinese_and_special_characters.48a8=Login name ca
 i18n.cannot_join_cluster_as_role.01d4=Cannot join cluster as {} role
 i18n.not_an_enumeration.8244=Not an enumeration
 i18n.script_not_exist.b180=The corresponding script no longer exists
+i18n.file_not_exist_or_unable_to_open.b045=The file does not exist or cannot be opened.
 i18n.already_offline.d3b5=It's offline.
 i18n.rebuild_success.5938=Rebuild successfully
 i18n.hours.2de0=hour
@@ -328,6 +335,7 @@ i18n.upload_progress_template.ac3f=Upload file progress \:{}/{} {}
 i18n.mark_already_exists.0ccc=tag already exists
 i18n.docker_not_found.2a2e=\ No docker found. Maybe the docker tag is filled in incorrectly, you need to configure the tag for docker.
 i18n.download_remote_file_failed.fcc3=Failed to download remote file\:
+i18n.no_matching_asset_ftp.d420=There is no corresponding asset FTP.
 i18n.no_file_found.6f1b=No {} file found
 i18n.distribute_result.a230=Distribution result\: {}
 i18n.multiple_ssh_addresses_found.b3f7=SSH address {} has multiple data, and configuration information using {} SSH will be automatically merged
@@ -335,6 +343,7 @@ i18n.no_management_permission.fd25=You do not have the corresponding administrat
 i18n.workspace_env_vars.f7e8=Workspace environment variables
 i18n.unsupported_mode.a3d3=Unsupported modes\:
 i18n.select_node_and_project.6021=Please select a node and project
+i18n.initialize_sql.6691=Execute the initialization SQL file
 i18n.operation_succeeded.3313=Operation successful
 i18n.url_length_exceeded.ca1c=The URL length cannot exceed 200.
 i18n.waiting_to_close_process.3634=Waiting to close the [Process] process\: {}
@@ -364,6 +373,7 @@ i18n.webhooks_invocation_error.9792=WebHooks call error
 i18n.missing_parent_id.4331=Missing Parent Task ID
 i18n.data_modification_time_format_incorrect.7ffe=The data modification time format is incorrect {} {}
 i18n.cannot_delete_recent_logs.ee19=Logs related to the past day cannot be deleted (file modification time)
+i18n.monitor_name.9aff=monitor
 i18n.agent_jar_not_exist.28ac=Agent JAR package does not exist
 i18n.parse_certificate_unknown_error.c43c=An unknown error occurred in parsing the certificate.
 i18n.node_info_incomplete.3b69=The corresponding node information is incomplete and cannot continue.
@@ -377,6 +387,7 @@ i18n.system_logs.84aa=system log
 i18n.machines_ssh_data_fixed.1387=Successfully repaired {} machine SSH data
 i18n.docker_label_required.b690=Please fill in the docker tag to execute
 i18n.no_matching_permission.09cf=Insufficient permissions not matched
+i18n.ftp_create_folder_exception.a4fe=FTP folder creation exception
 i18n.data_id_does_not_exist.a566=Data ID does not exist
 i18n.no_ssh_info.a8ec=No corresponding SSH information
 i18n.distribution_with_build_items_message.45f5=There are construction items in the current distribution and cannot be deleted directly (you need to unbind or delete the associated data in advance to delete it)
@@ -401,6 +412,7 @@ i18n.node_not_enabled.10ef={} Node is not enabled
 i18n.client_id_not_configured.ab8e=No clientId configured
 i18n.build_product_dir_not_empty.ba06=The bundle directory cannot be empty, length 1-200
 i18n.trigger_token_error_or_expired.8976=Triggered token error, or has expired
+i18n.ftp_connection_failed.1f2f=FTP connection failed
 i18n.auth_info_error.c184=Authorization information error
 i18n.download_file_error.5bcd=Abnormal download file\:
 i18n.rollback_ended.fb1d=End of execution rollback\: {}
@@ -416,6 +428,7 @@ i18n.id_already_exists.6208=ID already exists
 i18n.ssh_info.ebe6=SSH information
 i18n.current_status.81c0=\ Currently still available\:
 i18n.project_path_no_spaces.263c=The project path cannot contain spaces
+i18n.ftp_asset_management.c6a5=FTP Asset Management
 i18n.please_fill_in_address_of.9e02=Please fill in the address of% s
 i18n.rsa_private_key_file_invalid.5f12=The RSA private key file does not exist or is incorrect
 i18n.ssh_node_required.4566=Please select the ssh node
@@ -456,6 +469,7 @@ i18n.load_plugin.1f64=Load\: {} plugin
 i18n.host_field_required.5c36=Line {} The host field cannot be empty
 i18n.container_build_interrupted.a17b=Container build was interrupted\:
 i18n.upload_action.d5a7=upload
+i18n.ftp_connection_or_operation_exception.09af=FTP connection or operation is abnormal
 i18n.delete_file_failure.041f=Failed to delete file, please check
 i18n.no_cluster_info_found.fb40=The corresponding cluster information was not found.
 i18n.script_template_log.30cb=Script template log
@@ -509,6 +523,7 @@ i18n.invalid_zip_file.3092=The compressed package uploaded is not a Jpom [{}] pa
 i18n.publish_command_non_zero_exit_code.ea80=Execute the publish command Exit code is not 0, {}
 i18n.project_path_promotion_issue.2250=There is an issue with the promoted directory in the project path
 i18n.handle_node_deletion_script_failure_duplicate.821e=Failed to process {} node deletion script {}
+i18n.ftp_folder_query_failed.0011=Unable to query folder FTP,
 i18n.no_matching_process_type.b468=Did not match the appropriate processing type
 i18n.unsupported_type.7495=Unsupported types
 i18n.submit_task_queue_success.5f5b=Submit task queue successfully, current queue number\:
@@ -567,6 +582,7 @@ i18n.select_correct_pre_publish_script.d230=Please select the correct pre-releas
 i18n.greeting.5ecd=Hello, Jpom.
 i18n.ssh_connection_failed.4719=SSH connection failed
 i18n.oauth2_redirect_failed.6dcd=Failed to jump to oauth2, {} {}
+i18n.ftp_upload_failed.8298=FTP upload failed
 i18n.data_workspace_mismatch.ae1d=The data workspace and the operation workspace are inconsistent
 i18n.process_file_event_exception.e8e6=Handling file event exceptions
 i18n.current_docker_cluster_has_no_management_nodes_online.56cd=The current {} docker cluster has no management nodes online
@@ -578,6 +594,7 @@ i18n.restart_operation.5e3a=Perform a restart operation
 i18n.no_h2_data_info_for_migration.5799=No h2 data information does not need to be migrated
 i18n.publish_success.2fff=Published successfully
 i18n.system_cache.c4a8=system cache
+i18n.no_ftp_item.8e39=No corresponding ftp entry
 i18n.distribution_machine_required.5921=Please select the machine to distribute
 i18n.build_call_container_exception.6e04=build call container exception
 i18n.process_killed_successfully.a4c3=Successful kill
@@ -586,6 +603,7 @@ i18n.auto_clear_data_errors.112f=Automatically clear data errors {} {}
 i18n.publish_directory_is_empty.79c6=Publish directory is empty
 i18n.file_or_directory_not_found.f03e=File does not exist or is a directory\:
 i18n.clear_file_cache_failed.5cd1=Failed to clear file cache
+i18n.database_backup_complete_path.861b=The database backup is complete, and the save path is
 i18n.file_downloading_status.c995=File downloading\:
 i18n.system_IP_authorization.9c08=System configuration IP authorization
 i18n.node_failed.20d5=Node failed\:
@@ -613,6 +631,7 @@ i18n.delete_failure_with_colon_and_full_stop.bc42=Delete failed\:
 i18n.product_directory_cannot_skip_levels.3ad4=The product catalog cannot be upgraded\:
 i18n.fix_null_workspace_data.4d0b=Fix data {} {} with null workspace
 i18n.external_config_file_path.06ec=external profile path
+i18n.asset_ftp_info.3b75=Asset FTP information
 i18n.soft_link_project_department_exists.fa97=The project department of Soft Chain exists
 i18n.docker_info.00d2=Docker information
 i18n.log_file_does_not_exist.f6c6=Log file does not exist
@@ -621,6 +640,7 @@ i18n.no_file_info.db01=There is no corresponding file information.
 i18n.record_operation_log_exception.8012=Log operation log exception
 i18n.corresponding_file_required.57b3=Please select the corresponding file
 i18n.build_command_no_delete.df52=Build commands cannot contain delete commands
+i18n.ftp_info_table.b177=FTP information table
 i18n.file_merge_error.f32f=Abnormal after file merging, incomplete files may be damaged.
 i18n.script_info_not_found.bd8d=No corresponding script information found.
 i18n.cannot_modify_own_info.4036=You can't modify your information.
@@ -675,6 +695,7 @@ i18n.container_build_host_config_conversion_failure.27aa=Container build hostCon
 i18n.build_task_count_and_queue_count.f0b6=Number of tasks currently under construction\: {}, Number of tasks in queue\: {} {}
 i18n.node_cache.d68c=Node cache
 i18n.cluster_node_not_in_system.0645=The node corresponding to the current cluster cannot exit the cluster if it is not in this system.
+i18n.ftp_selection.c903=Please choose to distribute FTP items
 i18n.file_search_failed.231b=File search failed
 i18n.exported_ssh_data.ce88=Exported ssh data
 i18n.execution_ended_with_duration.a59b=Execute the end {} process, time consuming\: {}
@@ -703,6 +724,7 @@ i18n.ssh_batch_command_execution_exception.029a=SSH batch execution command exce
 i18n.content_type_not_supported.81a9=unsupported contentType
 i18n.cloud_server_network_issues.a865=Troubleshooting and positioning of network-related issues such as security group configuration of Cloud as a Service.
 i18n.start_building_image.eacd={} Start building the mirror {} {}
+i18n.ftp_already_exists.d66b=The corresponding FTP already exists
 i18n.configure_correct_redirect_url.058e=Please configure the correct redirect URL.
 i18n.no_cache_info_with_minus_one.52f2=No corresponding cache information\: -1
 i18n.no_node_entry_found.b1ef=No corresponding node item found\: {}
@@ -714,6 +736,7 @@ i18n.no_docker_info_found.6d38=No corresponding docker information was found.
 i18n.update_docker_machine_id_failed.063d=Updating DOCKER table machine id failed\:
 i18n.build_status_abnormal.8ca1=The build status is abnormal or cancelled
 i18n.node_connection_failure.896d=Node connection failed, please check if the node is online.
+i18n.database_load_success_url.5f64=The database loaded successfully and the URL is
 i18n.login_name_already_exists.2511=Login name already exists
 i18n.port_error.312e=port error
 i18n.account_disabled.9361=The account has been disabled and cannot be used.
@@ -763,9 +786,12 @@ i18n.error_sql.15ff=Error sql\: {}
 i18n.verification_method_not_configured.7358={} Authentication method not configured\: {}
 i18n.local_git_certificate_not_supported.b395=Local git-specified certificate pull code is temporarily not supported
 i18n.unsupported_method_with_colon.eae8=Unsupported methods\:
+i18n.exported_ftp_data.2b54=Exported ftp data
 i18n.current_docker_has_no_cluster_info.0b52=The current docker has no cluster information.
 i18n.cannot_delete_self.fec9=Can't delete yourself
 i18n.operation_monitoring_error.8036=execution action monitoring error
+i18n.ftp_read_file_failed.e738=FTP read file failed
+i18n.file_exists.145b=The folder or file already exists
 i18n.alias_or_token_error.d5c6=The alias or token is wrong, or has expired
 i18n.version_config_info.7b29=version configuration information
 i18n.delete_old_package.ca95=Delete the old package\: {}
@@ -778,6 +804,7 @@ i18n.node_service_stopped_failed_restart.4307=The [{}] project {} of the [{}] no
 i18n.associated_workspace.885b=Affiliated workspace
 i18n.auto_clear_machine_node_stats_logs.5279=Automatically clean up {} machine node statistical logs
 i18n.unsupported_mode.501d=Unsupported mode
+i18n.ftp_client_build_failure.aa55=Failed to build FTP client side [{}]\: {}
 i18n.missing_script_message.af89=No corresponding script found
 i18n.start_migrating.20d6=Start migration {} {}
 i18n.incompatible_database_version.8f7b=Database versions are not compatible, and cross-version upgrades need to be handled.
@@ -802,6 +829,7 @@ i18n.configure_user_notification.250d=Please configure user notifications
 i18n.operation_status_code.8231=operation status code
 i18n.agent_jar_damaged.74a8=Agent JAR damaged, please re-upload,
 i18n.read_error.7fa5=Read error
+i18n.import_success.ef46=Import successful\: {}
 i18n.network_resource_monitoring_error.4ede=Network interface card resource monitoring abnormality\:
 i18n.select_workspace_to_modify.ac87=Please select the workspace you want to modify
 i18n.decode_failure.822e=Decoding failed
@@ -832,6 +860,8 @@ i18n.project_id_not_found.b87e=No project id.
 i18n.operation_type.de9c=operation type
 i18n.project_has_logs_cannot_migrate.2e0e=The current project has log reading and cannot be migrated directly.
 i18n.delete_failure_with_colon.b429=Delete failed\:
+i18n.no_ftp_server_correspondence.1af7=There is no corresponding Ftp.
+i18n.ftp_import_template.8fa3=FTP import template
 i18n.tag_cannot_contain_colon.f9ae=Labels cannot contain\:
 i18n.distribute_id_already_exists_globally.6478=The distribution id already exists, and the distribution id needs to be globally unique.
 i18n.import_exception.04b6=Import the {} data exception\: {}
@@ -895,7 +925,9 @@ i18n.no_manager_node_found.5934=No management nodes were found in the current cl
 i18n.file_transfer_exception.bda6=Forwarded file exception
 i18n.select_node.f8a6=Please select a node
 i18n.upload_progress_with_units.44ad=Upload file progress \:{} {}/{} {}
+i18n.ftp_download_file_failed.2e42=FTP download file failed
 i18n.ssh_terminal_execution_log.58f1=SSH end point execution log
+i18n.ftp_file_manager.c52e=FTP file management
 i18n.project_monitor.d2ff=project monitoring
 i18n.push_image_interrupted.6377=Push image is interrupted\:
 i18n.default_value.1aa9={} [Default]
@@ -986,6 +1018,7 @@ i18n.certificate_already_exists.adf9=The current certificate already exists (in
 i18n.oshi_file_system_monitoring_exception.dc24=Oshi file system resource monitoring exception
 i18n.image_cannot_be_empty.1600=Mirror image cannot be empty
 i18n.no_corresponding_node_info.cd24=There is no corresponding node information.
+i18n.auto_backup_path.a16b=Automatically backup data files to the path
 i18n.node_plugin_version_required.2318=Node plugin version cannot be empty
 i18n.start_new_thread_for_h2_database_backup.9337=Start a new thread to perform an H2 database backup
 i18n.docker_exec_terminal_process_ended.c734=[{}] docker exec end point
@@ -1005,6 +1038,7 @@ i18n.protocol_not_supported.b906=Unsupported protocols
 i18n.force_unbind_succeeded.5bfd=Forced unbinding successful
 i18n.unexpected_exception.2b52=Abnormality occurs
 i18n.check_email_error.636c=Check email information error\: {}
+i18n.ftp_unauthorized_directory.df73=This ftp is not authorized to operate on this directory
 i18n.initialization_success.4725=Initialization successful
 i18n.running_status.d679=Running
 i18n.auto_clean_temp_dir.11d2=Automatic cleaning of temporary directories
@@ -1101,6 +1135,7 @@ i18n.batch_trigger_script_exception.8fb4=Server level script batch trigger excep
 i18n.upgrade_failure.4ae2=Upgrade failed
 i18n.no_branch_or_tag_message.8ae3=No {} branches/tags
 i18n.general_error_message.728a=Ah, something seems to be wrong, please try again later~
+i18n.ftp_connection_failure.0f31=Failed to close FTP connection
 i18n.service_info_incomplete_with_code2.e9ca=The service information is incomplete and cannot be operated\: -2
 i18n.ignored_operation.edee=Ignored operation\: {}
 i18n.monitor_name_cannot_be_empty.514a=Monitor name cannot be empty
@@ -1115,6 +1150,7 @@ i18n.select_related_data_id.7fba=Please select the associated data ID.
 i18n.node_name_required.5bdf=Please fill in the node name
 i18n.client_terminated_connection.6886=Client side terminates connection\: {}
 i18n.close_thread_pool.4cd9=Close the {} thread pool
+i18n.ftp_upload_file_exception.118c=Abnormal FTP upload file
 i18n.clear_ip_whitelist_config_success.8cf6=Clear IP whitelist Configuration successful
 i18n.no_corresponding_folder.621f=No corresponding folder
 i18n.no_online_manager_node_found.05d7=The current cluster does not find an online management node
@@ -1124,6 +1160,7 @@ i18n.execution_completed.24a1=Execution completed\:
 i18n.close_project_failure.a1d2=Failed to close the project\:
 i18n.select_folder_to_compress.915f=Please select a folder to compress
 i18n.config_file_not_found.fc87=No configuration file found
+i18n.ftp_item_not_found.60a7=FTP entry not found\: {}
 i18n.update_ssh_machine_id_failed.bd24=Updating SSH table machine id failed\:
 i18n.select_pull_code_protocol.fc24=Please select the protocol for pulling the code
 i18n.auth_directory_cannot_be_empty.21ba=The authorization directory cannot be empty
@@ -1291,6 +1328,7 @@ i18n.program_already_running.96e1=The current program is running and cannot be s
 i18n.password_change_success.8013=Password changed successfully\!
 i18n.build_resource_cleanup_failed.c4cf=Failed to clean up build resources
 i18n.incorrect_publish_method.e095=Incorrect publishing method
+i18n.ftp_directory.a790=Please enter the directory to publish to ftp
 i18n.import_low_version_data_to_new_version.247b=2. Import the exported lower version data (sql file) into the new version [Add --replace-import-h2-sql\=/xxxx.sql to the startup parameters (the path needs to be replaced with the sql file save path output by the console in the first step) ]
 i18n.incorrect_line_number.5877=Line number is incorrect
 i18n.file_deletion_event_with_details.7537=File deletion event\: {} {}
@@ -1299,6 +1337,7 @@ i18n.maven_plugin_version_required.71f1=Maven plugin version cannot be empty
 i18n.trigger_token_error_or_expired_with_code.393b=Triggered token error, or has expired\: -1
 i18n.invalid_webhooks_address.d836=WebHooks address is invalid
 i18n.project_path_auth_not_under_jpom.0e18=Project path authorization cannot be located in the Jpom directory
+i18n.ftp_rename_failed_exception.0fcc=FTP rename failed exception
 i18n.reconnect_failure.7c01=Reconnect failed
 i18n.project_operations.03d9=Project operation and maintenance
 i18n.image_not_exist.ee17=Mirror does not exist
@@ -1338,6 +1377,7 @@ i18n.refreshing_cache.c969=The cache is being refreshed, please do not refresh r
 i18n.start_executing_database_event.fc57=Start executing database events\: {}
 i18n.static_file_storage.35f6=static file storage
 i18n.project_log_is_existing_folder.a80a=The project log is an existing folder
+i18n.check_ftp_connection_failed.f7de=Detect FTP connection failure [{}] and prepare to rebuild\: {}
 i18n.project_data_workspace_id_inconsistency.7ed6=Project data workspace ID [{}] query out that the node ID is inconsistent, old data\: {}, new data\: {}
 i18n.query_workspace_error.6a0d=Failed to query the wrong workspace
 i18n.unknown_database_mode.f9e5=The current database schema is unknown
@@ -1375,8 +1415,8 @@ i18n.token_invalid_or_expired.cb96=Token error, or has expired\: -1
 i18n.list_and_query.c783=List, query
 i18n.migration_docker_cert_error.a5ea=There was an exception migrating the docker [{}] certificate
 i18n.file_downloading.7a8f=File downloading
-i18n.mark_must_contain_letters_numbers_underscores.667d=Markers can only contain letters, numbers, and underscores
 i18n.please_fill_in_runs_on.c2ff=Please fill in runsOn.
+i18n.mark_must_contain_letters_numbers_underscores.667d=Markers can only contain letters, numbers, and underscores
 i18n.create_build_task_exception.06f1=Create build task exception
 i18n.execution_interrupted.1bb6=Execution was interrupted
 i18n.event_loss_or_execution_error.7b14=Event missing or execution error\: {} {}
@@ -1401,6 +1441,7 @@ i18n.please_pass_parameter.3182=Please pass in parameters
 i18n.node_no_data_pulled.0dae=The {} node did not pull any {}, but deleted the data\: {}
 i18n.node_info_not_found.2c8c=No node information was found\:
 i18n.please_fill_in_repository_address.0cf8=Please fill in the warehouse address.
+i18n.no_ftp_correspondence.23c4=There is no corresponding FTP.
 i18n.table_without_primary_key.7392=Table has no primary key
 i18n.invalid_shard_id.46fd=Illegal sharding id
 i18n.execution_ended.b793=End of execution\: {}
@@ -1499,6 +1540,7 @@ i18n.config_file_database_config_not_parsed.47b2=Database configuration informat
 i18n.data_already_exists.0397=The imported data already exists
 i18n.please_check_in_time.3b4f=Please check in time.
 i18n.installation_success.811f=Installation successful
+i18n.timeout.e944=timeout
 i18n.cluster_management.74ea=cluster management
 i18n.type_field_value_error.14cf=Line {} Type field value error (Git/Svn)
 i18n.migrate_data.f556=migrate data
@@ -1514,6 +1556,7 @@ i18n.backup_data_trigger.a71a=backup data trigger
 i18n.file_signature_info_not_found.83bf=No file signature information
 i18n.associated_nodes_warning.64d8=The current machine is also associated with {} nodes and cannot be deleted directly (you need to unbind or delete the associated data in advance to delete it)
 i18n.build_unknown_error.dad6=An unknown error occurred in the build
+i18n.start_import_data.ea31=Start importing data\: {}
 i18n.new_version_exists_download_unavailable.4ba7=There is a new version, and the download address is not available.
 i18n.compare_backup_failure.303e=Comparison Empty project file backup failed
 i18n.upload_success_and_restart.7bc3=Upload successful and restart
@@ -1547,9 +1590,11 @@ i18n.download_exception.e616=Abnormal download file
 i18n.process_file_deletion_exception.1c6e=Handling file deletion exceptions
 i18n.trigger_auto_execute_command_template_exception.4e01=Trigger an autoexecute command template exception
 i18n.no_build.d163=No corresponding build
+i18n.ftp_management.cb91=FTP management
 i18n.start_executing_publishing_with_file_size.5039=To start publishing, the file size that needs to be published\: {}
 i18n.command_execution_exception.4ccd=Execution command exception
 i18n.operation_ip.cbd4=Operate IP
+i18n.exception.c195=abnormal
 i18n.auth_directory_cannot_contain_hierarchy.d6ca=Inclusion relationships cannot exist in the authorization directory.
 i18n.ip_authorization_interception_exception.8130=The IP authorization interception is abnormal, please check whether the configuration is correct.
 i18n.refresh_token_timeout.3291=Refresh token timeout

+ 51 - 6
modules/common/src/main/resources/i18n/messages_zh_CN.properties

@@ -1,5 +1,5 @@
 #i18n zh
-#Thu Jan 09 16:59:38 CST 2025
+#Thu Jun 12 16:41:03 CST 2025
 i18n.ssh_info_does_not_exist.5ed0=ssh 信息不存在啦
 i18n.incompatible_program_versions.5291=当前程序版本 {} 新版程序最低兼容 {} 不能直接升级
 i18n.no_projects_configured.e873=没有配置任何项目
@@ -8,6 +8,7 @@ i18n.machine_ssh_info.8dbb=机器SSH信息
 i18n.machine_info_not_exist.3468=对应的机器信息不存在
 i18n.unsupported_method.a1de=不支持的方式
 i18n.delete_local_image_failed.91fa=删除本地镜像失败
+i18n.cluster_not_grouped.8f54=当前集群还未绑定分组,不能监控 FTP 资产信息
 i18n.service_name_in_cluster_required.5446=请填写集群中的服务名
 i18n.cluster_manager_node_not_found.1cd0=没有找到集群管理节点
 i18n.distribute_id_already_exists.2168=分发id已经存在啦
@@ -34,6 +35,7 @@ i18n.start_waiting_for_data_migration.e76f=开始等待数据迁移
 i18n.empty_file_or_folder_for_publish.cae8=发布的文件或者文件夹为空,不能继续发布
 i18n.repository_type_required.9414=请选择仓库类型
 i18n.demo_account_cannot_use_feature.a1a1=演示账号不能使用该功能
+i18n.start_backup_database.e554=开始备份数据库
 i18n.async_resource_expired.2ddc=异步资源过期,需要主动关闭,{} {}
 i18n.mark_cannot_be_empty.1927=标记不能为空
 i18n.get_success.fb55=获取成功
@@ -50,9 +52,11 @@ i18n.upload_failed_no_matching_project.b219=上传失败,没有找到对应的
 i18n.no_branches_or_tags_in_repository.76b6=仓库没有任何分支或者标签
 i18n.download_file_description.10cb=下载文件 {} {} {}
 i18n.delay_build.7d62=延迟 {} 秒后开始构建
+i18n.ftp_not_exist.f9b3=不存在对应ftp
 i18n.default_cluster.38cf=默认集群
 i18n.node_and_check_project_failed.ac4b=节点与检查项目失败
 i18n.load_oauth2_config.da42=加载 oauth2 配置 :{} {}
+i18n.affected_rows.5781=影响行数
 i18n.cluster_not_bound_to_group_for_node_monitoring.1586=当前集群还未绑定分组,不能监控集群节点资产信息
 i18n.cannot_execute_error.4c29=不能执行:error
 i18n.no_environment_variable.c79f=没有对应的环境变量
@@ -114,6 +118,7 @@ i18n.no_ssh_entry_found.d0e1=没有找到对应的ssh项:{}
 i18n.build_runs_on_image_interrupted.00fd=构建 runsOn 镜像被中断
 i18n.incorrect_parameter_format.9efb=传入的参数格式不正确
 i18n.multiple_certificate_files_found.bee3=找到 2 个以上的证书文件
+i18n.file_not_exist_or_unable_to_download.b977=文件不存在或无法下载\:
 i18n.invalid_runs_on_image_name.4b96=runsOn 镜像名称不合法
 i18n.dockerfile_path_required.69ac=请填写要执行的 Dockerfile 路径
 i18n.file_upload_mode_not_configured.b3b2=没有配置文件上传模式
@@ -248,6 +253,7 @@ i18n.select_node_to_modify.6617=请选择要修改的节
 i18n.correct_dingtalk_address_required.2b4a=请输入正确钉钉地址
 i18n.query_folder_sftp_failed.9d35=查询文件夹 SFTP 失败,
 i18n.name_field_required.e0c5=第 {} 行 name 字段不能位空
+i18n.ftp_connection_failed_message.bd99=连接FTP失败:
 i18n.start_building_with_number_and_path.c41c=开始构建 \#{} 构建执行路径 \: {}
 i18n.plugin_end_log_connection_successful.9035=连接成功:插件端日志
 i18n.start_executing_upload_pre_command.fb5c=开始执行上传前命令
@@ -293,6 +299,7 @@ i18n.cannot_join_cluster_as_role.01d4=不能以 {} 角色加入集群
 i18n.not_an_enumeration.8244=不是枚举
 i18n.script_not_exist.b180=对应脚本已经不存在啦
 i18n.rebuild_success.5938=重建成功
+i18n.file_not_exist_or_unable_to_open.b045=文件不存在或无法打开\:
 i18n.already_offline.d3b5=已经离线啦
 i18n.hours.2de0=小时
 i18n.project_management.4363=项目管理
@@ -328,6 +335,7 @@ i18n.upload_progress_template.ac3f=上传文件进度\:{}/{} {}
 i18n.mark_already_exists.0ccc=标记已存在
 i18n.docker_not_found.2a2e=\ 没有找到任何 docker。可能docker tag 填写不正确,需要为 docker 配置标签
 i18n.download_remote_file_failed.fcc3=下载远程文件失败\:
+i18n.no_matching_asset_ftp.d420=不存在对应的资产FTP
 i18n.no_file_found.6f1b=没有找到 {} 文件
 i18n.distribute_result.a230=分发结果:{}
 i18n.multiple_ssh_addresses_found.b3f7=SSH 地址 {} 存在多个数据,将自动合并使用 {} SSH的配置信息
@@ -335,6 +343,7 @@ i18n.no_management_permission.fd25=您没有对应管理权限\:-2
 i18n.workspace_env_vars.f7e8=工作空间环境变量
 i18n.unsupported_mode.a3d3=暂不支持的模式:
 i18n.select_node_and_project.6021=请选择节点和项目
+i18n.initialize_sql.6691=执行初始化SQL文件
 i18n.operation_succeeded.3313=操作成功
 i18n.waiting_to_close_process.3634=等待关闭[Process]进程:{}
 i18n.url_length_exceeded.ca1c=url 长度不能超过 200
@@ -364,6 +373,7 @@ i18n.webhooks_invocation_error.9792=WebHooks 调用错误
 i18n.missing_parent_id.4331=父任务id缺失
 i18n.data_modification_time_format_incorrect.7ffe=数据修改时间格式不正确 {} {}
 i18n.cannot_delete_recent_logs.ee19=不能删除近一天相关的日志(文件修改时间)
+i18n.monitor_name.9aff=监控
 i18n.agent_jar_not_exist.28ac=Agent JAR包不存在
 i18n.parse_certificate_unknown_error.c43c=解析证书发生未知错误:
 i18n.node_info_incomplete.3b69=对应的节点信息不完整不能继续
@@ -377,12 +387,13 @@ i18n.system_logs.84aa=系统日志
 i18n.machines_ssh_data_fixed.1387=成功修复 {} 条机器 SSH 数据
 i18n.docker_label_required.b690=请填要执行 docker 标签
 i18n.no_matching_permission.09cf=未匹配到合适的权限不足
+i18n.ftp_create_folder_exception.a4fe=FTP创建文件夹异常
 i18n.data_id_does_not_exist.a566=数据id 不存在
 i18n.no_ssh_info.a8ec=没有对应 SSH 信息
 i18n.distribution_with_build_items_message.45f5=当前分发存在构建项,不能直接删除(需要提前解绑或者删除关联数据后才能删除)
 i18n.scanning_in_progress.7444=当前正在扫描中
-i18n.day_or_sky.249a=天
 i18n.please_pass_body_parameter.4e5c=请传入 body 参数
+i18n.day_or_sky.249a=天
 i18n.get_decrypt_distribution_failure.4feb=获取解密分发失败
 i18n.retention_days.3c7d=,保留天数:{}
 i18n.no_log_info.d551=还没有日志信息
@@ -401,6 +412,7 @@ i18n.node_not_enabled.10ef={} 节点未启用
 i18n.client_id_not_configured.ab8e=没有配置 clientId
 i18n.build_product_dir_not_empty.ba06=构建产物目录不能为空,长度1-200
 i18n.trigger_token_error_or_expired.8976=触发token错误,或者已经失效
+i18n.ftp_connection_failed.1f2f=连接FTP失败
 i18n.auth_info_error.c184=授权信息错误
 i18n.download_file_error.5bcd=下载文件异常\:
 i18n.rollback_ended.fb1d=执行回滚结束:{}
@@ -416,6 +428,7 @@ i18n.ssh_info.ebe6=SSH 信息
 i18n.id_already_exists.6208=id已经存在啦
 i18n.current_status.81c0=\ 当前还在:
 i18n.project_path_no_spaces.263c=项目路径不能包含空格
+i18n.ftp_asset_management.c6a5=FTP资产管理
 i18n.please_fill_in_address_of.9e02=请填写 %s 的 地址
 i18n.rsa_private_key_file_invalid.5f12=rsa 私钥文件不存在或者有误
 i18n.ssh_node_required.4566=请选择 ssh 节点
@@ -456,6 +469,7 @@ i18n.load_plugin.1f64=加载:{} 插件
 i18n.host_field_required.5c36=第 {} 行 host 字段不能位空
 i18n.container_build_interrupted.a17b=容器 build 被中断\:
 i18n.upload_action.d5a7=上传
+i18n.ftp_connection_or_operation_exception.09af=FTP 连接或操作异常
 i18n.delete_file_failure.041f=删除文件失败,请检查
 i18n.script_template_log.30cb=脚本模板日志
 i18n.no_cluster_info_found.fb40=没有找到对应的集群信息
@@ -509,6 +523,7 @@ i18n.invalid_zip_file.3092=上传的压缩包不是 Jpom [{}] 包
 i18n.publish_command_non_zero_exit_code.ea80=执行发布命令退出码非0,{}
 i18n.project_path_promotion_issue.2250=项目路径存在提升目录问题
 i18n.handle_node_deletion_script_failure_duplicate.821e=处理 {} 节点删除脚本失败{}
+i18n.ftp_folder_query_failed.0011=无法查询文件夹FTP,
 i18n.unsupported_type.7495=不支持的类型
 i18n.submit_task_queue_success.5f5b=提交任务队列成功,当前队列数:
 i18n.no_matching_process_type.b468=未匹配到合适的处理类型
@@ -567,6 +582,7 @@ i18n.select_correct_pre_publish_script.d230=请选择正确的发布前脚本
 i18n.greeting.5ecd=您好,Jpom
 i18n.ssh_connection_failed.4719=ssh连接失败
 i18n.oauth2_redirect_failed.6dcd=跳转 oauth2 失败,{} {}
+i18n.ftp_upload_failed.8298=FTP 上传失败
 i18n.data_workspace_mismatch.ae1d=数据工作空间和操作工作空间不一致
 i18n.process_file_event_exception.e8e6=处理文件事件异常
 i18n.current_docker_cluster_has_no_management_nodes_online.56cd=当前 {} docker 集群没有管理节点在线
@@ -578,6 +594,7 @@ i18n.cluster_status_code_exception.9d89=集群状态码异常:{} {}
 i18n.no_h2_data_info_for_migration.5799=没有 h2 数据信息不用迁移
 i18n.publish_success.2fff=发布成功
 i18n.system_cache.c4a8=系统缓存
+i18n.no_ftp_item.8e39=没有对应的ftp项
 i18n.distribution_machine_required.5921=请选择分发的机器
 i18n.build_call_container_exception.6e04=构建调用容器异常
 i18n.process_killed_successfully.a4c3=成功kill
@@ -586,6 +603,7 @@ i18n.auto_clear_data_errors.112f=自动清除数据错误 {} {}
 i18n.publish_directory_is_empty.79c6=发布目录为空
 i18n.file_or_directory_not_found.f03e=文件不存在或者是目录\:
 i18n.clear_file_cache_failed.5cd1=清空文件缓存失败
+i18n.database_backup_complete_path.861b=数据库备份完成,保存路径为
 i18n.file_downloading_status.c995=文件下载中:
 i18n.system_IP_authorization.9c08=系统配置IP授权
 i18n.node_failed.20d5=节点失败:
@@ -613,6 +631,7 @@ i18n.delete_failure_with_colon_and_full_stop.bc42=删除失败:
 i18n.product_directory_cannot_skip_levels.3ad4=产物目录不能越级:
 i18n.fix_null_workspace_data.4d0b=修复工作空间为 null 的数据 {} {}
 i18n.external_config_file_path.06ec=外部配置文件路径
+i18n.asset_ftp_info.3b75=资产FTP信息
 i18n.soft_link_project_department_exists.fa97=软链的项目部存在
 i18n.system_admin_not_found.6f6c=没有找到系统管理员
 i18n.docker_info.00d2=docker 信息
@@ -621,6 +640,7 @@ i18n.record_operation_log_exception.8012=记录操作日志异常
 i18n.no_file_info.db01=没有对应的文件信息
 i18n.corresponding_file_required.57b3=请选择对应到文件
 i18n.build_command_no_delete.df52=构建命令不能包含删除命令
+i18n.ftp_info_table.b177=ftp信息表
 i18n.file_merge_error.f32f=文件合并后异常,文件不完整可能被损坏
 i18n.script_info_not_found.bd8d=找不到对应的脚本信息
 i18n.cannot_modify_own_info.4036=不能修改自己的信息
@@ -637,11 +657,11 @@ i18n.check_docker_url_exception.4302=检查 docker url 异常 {}
 i18n.id_not_empty.bf1f=id不能为空
 i18n.execution_frequency.d014={} 秒执行一次
 i18n.parse_jar.a26e=解析jar
-i18n.jpom_verification_code.5b5b=Jpom 验证码
 i18n.please_fill_in_personal_token.970a=请填写个人令牌
+i18n.jpom_verification_code.5b5b=Jpom 验证码
 i18n.parent_table_info_config_error.2f52=父级表信息配置错误,
-i18n.error_message.44ce=可能是下载授权码错误或者对应授权码被禁用以及触发限流机制
 i18n.physical_node_pull_records.99df={} 物理节点拉取到 {} 个执行记录,更新 {} 个执行记录
+i18n.error_message.44ce=可能是下载授权码错误或者对应授权码被禁用以及触发限流机制
 i18n.login_success.71fa=登录成功
 i18n.clear_temp_file_failed_check_directory.7340=清除临时文件失败,请检查目录:
 i18n.request_failed_message.9c71=请求失败\: status\: %s body\: %s headers\: %s
@@ -675,6 +695,7 @@ i18n.container_build_host_config_conversion_failure.27aa=容器构建 hostConfig
 i18n.build_task_count_and_queue_count.f0b6=当前构建中任务数:{},队列中任务数:{} {}
 i18n.node_cache.d68c=节点缓存
 i18n.cluster_node_not_in_system.0645=当前集群对应的节点,不在本系统中无法退出集群
+i18n.ftp_selection.c903=请选择分发FTP项
 i18n.file_search_failed.231b=文件搜索失败
 i18n.exported_ssh_data.ce88=导出的 ssh 数据
 i18n.execution_ended_with_duration.a59b=执行结束 {}流程,耗时:{}
@@ -703,6 +724,7 @@ i18n.ssh_batch_command_execution_exception.029a=ssh 批量执行命令异常
 i18n.content_type_not_supported.81a9=不支持的 contentType
 i18n.cloud_server_network_issues.a865=云服务器的安全组配置等网络相关问题排查定位。
 i18n.start_building_image.eacd={} 开始构建镜像 {}{}
+i18n.ftp_already_exists.d66b=对应的FTP已经存在啦
 i18n.configure_correct_redirect_url.058e=请配置正确的重定向 url
 i18n.no_cache_info_with_minus_one.52f2=没有对应的缓存信息:-1
 i18n.no_node_entry_found.b1ef=没有找到对应的节点项:{}
@@ -714,6 +736,7 @@ i18n.no_docker_info_found.6d38=没有找到对应的 docker 信息
 i18n.update_docker_machine_id_failed.063d=更新 DOCKER 表机器id 失败:
 i18n.build_status_abnormal.8ca1=构建状态异常或者被取消
 i18n.node_connection_failure.896d=节点连接失败,请检查节点是否在线
+i18n.database_load_success_url.5f64=数据库加载成功,URL为
 i18n.login_name_already_exists.2511=登录名已经存在
 i18n.port_error.312e=端口错误
 i18n.account_disabled.9361=账号已经被禁用,不能使用
@@ -744,8 +767,8 @@ i18n.cron_expression_incorrect.b41a=cron 表达式不正确,
 i18n.script_template_id_required.f339=请填写脚本模板id
 i18n.start_rolling_back.f020=开始回滚:{}
 i18n.execution_exception.b0d5=执行异常
-i18n.corresponding_function.5bb5=对应功能【{}-{}】
 i18n.plugin_parameter_incorrect.a355=插件端使用参数不正确
+i18n.corresponding_function.5bb5=对应功能【{}-{}】
 i18n.operation_failed.3d94=操作失败 
 i18n.start_publishing.c0b9=开始发布中
 i18n.monitor_info.f299=监控信息
@@ -763,9 +786,12 @@ i18n.error_sql.15ff=错误 sql\:{}
 i18n.verification_method_not_configured.7358={}未配置验证方法:{}
 i18n.local_git_certificate_not_supported.b395=暂时不支持本地 git 指定证书拉取代码
 i18n.unsupported_method_with_colon.eae8=不支持的方式:
+i18n.exported_ftp_data.2b54=导出的 ftp 数据
 i18n.current_docker_has_no_cluster_info.0b52=当前 docker 没有集群信息
 i18n.cannot_delete_self.fec9=不能删除自己
 i18n.operation_monitoring_error.8036=执行操作监控错误
+i18n.ftp_read_file_failed.e738=FTP 读取文件失败
+i18n.file_exists.145b=文件夹或文件已经存在
 i18n.alias_or_token_error.d5c6=别名或者token错误,或者已经失效
 i18n.version_config_info.7b29=版本配置信息
 i18n.delete_old_package.ca95=删除旧程序包:{}
@@ -778,6 +804,7 @@ i18n.node_service_stopped_failed_restart.4307=【{}】节点的【{}】项目{}
 i18n.associated_workspace.885b=所属工作空间
 i18n.auto_clear_machine_node_stats_logs.5279=自动清理 {} 条机器节点统计日志
 i18n.unsupported_mode.501d=不支持的模式
+i18n.ftp_client_build_failure.aa55=构建 FTP 客户端失败 [{}]\: {}
 i18n.missing_script_message.af89=找不到对应的脚本
 i18n.start_migrating.20d6=开始迁移 {} {}
 i18n.incompatible_database_version.8f7b=数据库版本不兼容,需要处理跨版本升级。
@@ -802,6 +829,7 @@ i18n.configure_user_notification.250d=请配置用户通知
 i18n.operation_status_code.8231=操作状态码
 i18n.agent_jar_damaged.74a8=Agent JAR 损坏请重新上传,
 i18n.read_error.7fa5=读取错误
+i18n.import_success.ef46=导入成功:{}
 i18n.select_workspace_to_modify.ac87=请选择要修改的工作空间
 i18n.network_resource_monitoring_error.4ede=网卡资源监控异常:
 i18n.decode_failure.822e=解码失败
@@ -832,7 +860,9 @@ i18n.project_id_not_found.b87e=没有项目id
 i18n.operation_type.de9c=操作类型
 i18n.project_has_logs_cannot_migrate.2e0e=当前项目存在日志阅读,不能直接迁移
 i18n.delete_failure_with_colon.b429=删除失败\:
+i18n.no_ftp_server_correspondence.1af7=没有对应的Ftp
 i18n.tag_cannot_contain_colon.f9ae=标签不能包含 :
+i18n.ftp_import_template.8fa3=ftp导入模板
 i18n.distribute_id_already_exists_globally.6478=分发id已经存在啦,分发id需要全局唯一
 i18n.import_exception.04b6=导入第 {} 条数据异常\:{}
 i18n.script_template_log2.6b2c=脚本模版日志
@@ -895,7 +925,9 @@ i18n.no_manager_node_found.5934=当前集群未找到任何管理节点
 i18n.file_transfer_exception.bda6=转发文件异常
 i18n.upload_progress_with_units.44ad=上传文件进度\:{} {}/{} {} 
 i18n.select_node.f8a6=请选择节点
+i18n.ftp_download_file_failed.2e42=FTP 下载文件失败
 i18n.ssh_terminal_execution_log.58f1=ssh 终端执行日志
+i18n.ftp_file_manager.c52e=FTP文件管理
 i18n.push_image_interrupted.6377=push image 被中断\:
 i18n.project_monitor.d2ff=项目监控
 i18n.default_value.1aa9={} [默认]
@@ -986,6 +1018,7 @@ i18n.certificate_already_exists.adf9=当前证书已经存在啦(系统全局范
 i18n.oshi_file_system_monitoring_exception.dc24=oshi 文件系统资源监控异常
 i18n.image_cannot_be_empty.1600=镜像不能为空
 i18n.no_corresponding_node_info.cd24=没有对应到节点信息
+i18n.auto_backup_path.a16b=自动备份数据文件到路径
 i18n.start_new_thread_for_h2_database_backup.9337=启动一个新线程来执行 H2 数据库备份...启动
 i18n.node_plugin_version_required.2318=node 插件 version 不能为空
 i18n.docker_exec_terminal_process_ended.c734=[{}] docker exec 终端进程结束
@@ -1005,6 +1038,7 @@ i18n.protocol_not_supported.b906=不支持的 protocol
 i18n.force_unbind_succeeded.5bfd=强制解绑成功
 i18n.unexpected_exception.2b52=发生异常
 i18n.check_email_error.636c=检查邮箱信息错误:{}
+i18n.ftp_unauthorized_directory.df73=此ftp未授权操作此目录
 i18n.initialization_success.4725=初始化成功
 i18n.running_status.d679=运行中
 i18n.auto_clean_temp_dir.11d2=自动清理临时目录
@@ -1101,6 +1135,7 @@ i18n.upgrade_failure.4ae2=升级失败
 i18n.batch_trigger_script_exception.8fb4=服务端脚本批量触发异常
 i18n.no_branch_or_tag_message.8ae3=没有 {} 分支/标签
 i18n.general_error_message.728a=啊哦,好像哪里出错了,请稍候再试试吧~
+i18n.ftp_connection_failure.0f31=关闭 FTP 连接失败
 i18n.service_info_incomplete_with_code2.e9ca=服务信息不完整不能操作:-2
 i18n.ignored_operation.edee=忽略的操作:{}
 i18n.monitor_name_cannot_be_empty.514a=监控名称不能为空
@@ -1115,6 +1150,7 @@ i18n.select_related_data_id.7fba=请选择关联数据 ID
 i18n.node_name_required.5bdf=请填写节点名称
 i18n.client_terminated_connection.6886=客户端终止连接:{}
 i18n.close_thread_pool.4cd9=关闭 {} 线程池
+i18n.ftp_upload_file_exception.118c=FTP上传文件异常
 i18n.clear_ip_whitelist_config_success.8cf6=清除 IP 白名单配置成功
 i18n.no_corresponding_folder.621f=没有对应文件夹
 i18n.no_online_manager_node_found.05d7=当前集群未找到在线的管理节点
@@ -1125,6 +1161,7 @@ i18n.close_project_failure.a1d2=关闭项目失败:
 i18n.select_folder_to_compress.915f=请选择文件夹进行压缩
 i18n.config_file_not_found.fc87=均未找到配置文件
 i18n.update_ssh_machine_id_failed.bd24=更新 SSH 表机器id 失败:
+i18n.ftp_item_not_found.60a7=没有找到对应的ftp项:{}
 i18n.select_pull_code_protocol.fc24=请选择拉取代码的协议
 i18n.auth_directory_cannot_be_empty.21ba=授权目录不能为空
 i18n.status_not_in_progress.f410=当前状态不在进行中,
@@ -1291,6 +1328,7 @@ i18n.program_already_running.96e1=当前程序正在运行中,不能重复启
 i18n.password_change_success.8013=修改密码成功!
 i18n.build_resource_cleanup_failed.c4cf=清理构建资源失败
 i18n.incorrect_publish_method.e095=发布方法不正确
+i18n.ftp_directory.a790=请输入发布到ftp中的目录
 i18n.import_low_version_data_to_new_version.247b=2. 将导出的低版本数据( sql 文件) 导入到新版本中【启动程序参数里面添加 --replace-import-h2-sql\=/xxxx.sql (路径需要替换为第一步控制台输出的 sql 文件保存路径)】
 i18n.incorrect_line_number.5877=行号不正确
 i18n.file_deletion_event_with_details.7537=文件删除事件:{} {}
@@ -1299,6 +1337,7 @@ i18n.maven_plugin_version_required.71f1=maven 插件 version 不能为空
 i18n.trigger_token_error_or_expired_with_code.393b=触发token错误,或者已经失效\:-1
 i18n.invalid_webhooks_address.d836=WebHooks 地址不合法
 i18n.project_path_auth_not_under_jpom.0e18=项目路径授权不能位于Jpom目录下
+i18n.ftp_rename_failed_exception.0fcc=FTP重命名失败异常
 i18n.reconnect_failure.7c01=重连失败
 i18n.project_operations.03d9=项目运维
 i18n.image_not_exist.ee17=镜像不存在
@@ -1339,6 +1378,7 @@ i18n.start_executing_database_event.fc57=开始执行数据库事件:{}
 i18n.static_file_storage.35f6=静态文件存储
 i18n.project_log_is_existing_folder.a80a=项目log是一个已经存在的文件夹
 i18n.project_data_workspace_id_inconsistency.7ed6=项目数据工作空间ID[{}]查询出节点ID不一致, 旧数据\: {}, 新数据\: {}
+i18n.check_ftp_connection_failed.f7de=检测 FTP 连接失效 [{}],准备重建\: {}
 i18n.query_workspace_error.6a0d=查询错误的工作空间失败
 i18n.unknown_database_mode.f9e5=当前数据库模式未知
 i18n.secondary_directory_match.0aec={} 二级目录模糊匹配到 {} 个文件, 当前文件保留方式 {}
@@ -1375,8 +1415,8 @@ i18n.token_invalid_or_expired.cb96=token错误,或者已经失效\:-1
 i18n.list_and_query.c783=列表、查询
 i18n.migration_docker_cert_error.a5ea=迁移 docker[{}] 证书发生异常
 i18n.file_downloading.7a8f=文件下载中
-i18n.mark_must_contain_letters_numbers_underscores.667d=标记只能包含字母、数字、下划线
 i18n.please_fill_in_runs_on.c2ff=请填写runsOn。
+i18n.mark_must_contain_letters_numbers_underscores.667d=标记只能包含字母、数字、下划线
 i18n.create_build_task_exception.06f1=创建构建任务异常
 i18n.execution_interrupted.1bb6=执行被中断
 i18n.event_loss_or_execution_error.7b14=事件丢失或者执行错误:{} {}
@@ -1401,6 +1441,7 @@ i18n.please_pass_parameter.3182=请传入参数
 i18n.node_no_data_pulled.0dae={} 节点没有拉取到任何 {},但是删除了数据:{}
 i18n.node_info_not_found.2c8c=没有查询到节点信息:
 i18n.please_fill_in_repository_address.0cf8=请填写仓库地址
+i18n.no_ftp_correspondence.23c4=没有对应的FTP
 i18n.table_without_primary_key.7392=表没有主键
 i18n.invalid_shard_id.46fd=不合法的分片id
 i18n.execution_ended.b793=执行结束\:{}
@@ -1499,6 +1540,7 @@ i18n.config_file_database_config_not_parsed.47b2=未解析出配置文件中的
 i18n.data_already_exists.0397=导入的数据已经存在啦
 i18n.please_check_in_time.3b4f=请及时检查
 i18n.installation_success.811f=安装成功
+i18n.timeout.e944=超时
 i18n.cluster_management.74ea=集群管理
 i18n.type_field_value_error.14cf=第 {} 行 type 字段值错误(Git/Svn)
 i18n.migrate_data.f556=迁移数据
@@ -1514,6 +1556,7 @@ i18n.backup_data_trigger.a71a=备份数据触发器
 i18n.file_signature_info_not_found.83bf=没有文件签名信息
 i18n.associated_nodes_warning.64d8=当前机器还关联{}个节点,不能直接删除(需要提前解绑或者删除关联数据后才能删除)
 i18n.build_unknown_error.dad6=构建发生未知错误
+i18n.start_import_data.ea31=开始导入数据:{}
 i18n.new_version_exists_download_unavailable.4ba7=存在新版本,下载地址不可用
 i18n.compare_backup_failure.303e=对比清空项目文件备份失败
 i18n.upload_success_and_restart.7bc3=上传成功并重启
@@ -1547,9 +1590,11 @@ i18n.download_exception.e616=下载文件异常
 i18n.process_file_deletion_exception.1c6e=处理文件删除异常
 i18n.trigger_auto_execute_command_template_exception.4e01=触发自动执行命令模版异常
 i18n.no_build.d163=没有对应的构建
+i18n.ftp_management.cb91=FTP管理
 i18n.start_executing_publishing_with_file_size.5039=开始执行发布,需要发布的文件大小:{}
 i18n.command_execution_exception.4ccd=执行命令异常
 i18n.operation_ip.cbd4=操作IP
+i18n.exception.c195=异常
 i18n.auth_directory_cannot_contain_hierarchy.d6ca=授权目录中不能存在包含关系:
 i18n.ip_authorization_interception_exception.8130=IP授权拦截异常,请检查配置是否正确
 i18n.refresh_token_timeout.3291=刷新token超时

+ 47 - 2
modules/common/src/main/resources/i18n/messages_zh_HK.properties

@@ -1,5 +1,5 @@
 #i18n zh_HK
-#Thu Jan 09 16:59:42 CST 2025
+#Thu Jun 12 16:41:16 CST 2025
 i18n.ssh_info_does_not_exist.5ed0=ssh 信息不存在啦
 i18n.incompatible_program_versions.5291=當前程序版本 {} 新版程序最低兼容 {} 不能直接升級
 i18n.no_projects_configured.e873=沒有配置任何項目
@@ -8,6 +8,7 @@ i18n.machine_ssh_info.8dbb=機器SSH信息
 i18n.machine_info_not_exist.3468=對應的機器信息不存在
 i18n.unsupported_method.a1de=不支持的方式
 i18n.delete_local_image_failed.91fa=刪除本地鏡像失敗
+i18n.cluster_not_grouped.8f54=當前集羣還未綁定分組,不能監控 FTP 資產信息
 i18n.service_name_in_cluster_required.5446=請填寫集羣中的服務名
 i18n.cluster_manager_node_not_found.1cd0=沒有找到集羣管理節點
 i18n.distribute_id_already_exists.2168=分發id已經存在啦
@@ -34,6 +35,7 @@ i18n.start_waiting_for_data_migration.e76f=開始等待數據遷移
 i18n.empty_file_or_folder_for_publish.cae8=發佈的文件或者文件夾為空,不能繼續發佈
 i18n.demo_account_cannot_use_feature.a1a1=演示賬號不能使用該功能
 i18n.repository_type_required.9414=請選擇倉庫類型
+i18n.start_backup_database.e554=開始備份數據庫
 i18n.async_resource_expired.2ddc=異步資源過期,需要主動關閉,{} {}
 i18n.mark_cannot_be_empty.1927=標記不能為空
 i18n.get_success.fb55=獲取成功
@@ -50,9 +52,11 @@ i18n.upload_failed_no_matching_project.b219=上傳失敗,沒有找到對應的
 i18n.no_branches_or_tags_in_repository.76b6=倉庫沒有任何分支或者標籤
 i18n.download_file_description.10cb=下載文件 {} {} {}
 i18n.delay_build.7d62=延遲 {} 秒後開始構建
+i18n.ftp_not_exist.f9b3=不存在對應ftp
 i18n.default_cluster.38cf=默認集羣
 i18n.node_and_check_project_failed.ac4b=節點與檢查項目失敗
 i18n.load_oauth2_config.da42=加載 oauth2 配置 :{} {}
+i18n.affected_rows.5781=影響行數
 i18n.cluster_not_bound_to_group_for_node_monitoring.1586=當前集羣還未綁定分組,不能監控集羣節點資產信息
 i18n.cannot_execute_error.4c29=不能執行:error
 i18n.no_environment_variable.c79f=沒有對應的環境變量
@@ -114,6 +118,7 @@ i18n.no_ssh_entry_found.d0e1=沒有找到對應的ssh項:{}
 i18n.build_runs_on_image_interrupted.00fd=構建 runsOn 鏡像被中斷
 i18n.incorrect_parameter_format.9efb=傳入的參數格式不正確
 i18n.multiple_certificate_files_found.bee3=找到 2 個以上的證書文件
+i18n.file_not_exist_or_unable_to_download.b977=文件不存在或無法下載\:
 i18n.invalid_runs_on_image_name.4b96=runsOn 鏡像名稱不合法
 i18n.dockerfile_path_required.69ac=請填寫要執行的 Dockerfile 路徑
 i18n.file_upload_mode_not_configured.b3b2=沒有配置文件上傳模式
@@ -248,6 +253,7 @@ i18n.select_node_to_modify.6617=請選擇要修改的節
 i18n.correct_dingtalk_address_required.2b4a=請輸入正確釘釘地址
 i18n.query_folder_sftp_failed.9d35=查詢文件夾 SFTP 失敗,
 i18n.name_field_required.e0c5=第 {} 行 name 字段不能位空
+i18n.ftp_connection_failed_message.bd99=連接FTP失敗:
 i18n.plugin_end_log_connection_successful.9035=連接成功:插件端日誌
 i18n.start_building_with_number_and_path.c41c=開始構建 \#{} 構建執行路徑 \: {}
 i18n.start_executing_upload_pre_command.fb5c=開始執行上傳前命令
@@ -292,6 +298,7 @@ i18n.login_name_cannot_contain_chinese_and_special_characters.48a8=登錄名不
 i18n.cannot_join_cluster_as_role.01d4=不能以 {} 角色加入集羣
 i18n.not_an_enumeration.8244=不是枚舉
 i18n.script_not_exist.b180=對應腳本已經不存在啦
+i18n.file_not_exist_or_unable_to_open.b045=文件不存在或無法打開\:
 i18n.already_offline.d3b5=已經離線啦
 i18n.rebuild_success.5938=重建成功
 i18n.hours.2de0=小時
@@ -328,6 +335,7 @@ i18n.upload_progress_template.ac3f=上傳文件進度\:{}/{} {}
 i18n.mark_already_exists.0ccc=標記已存在
 i18n.docker_not_found.2a2e=\ 沒有找到任何 docker。可能docker tag 填寫不正確,需要為 docker 配置標籤
 i18n.download_remote_file_failed.fcc3=下載遠程文件失敗\:
+i18n.no_matching_asset_ftp.d420=不存在對應的資產FTP
 i18n.no_file_found.6f1b=沒有找到 {} 文件
 i18n.distribute_result.a230=分發結果:{}
 i18n.multiple_ssh_addresses_found.b3f7=SSH 地址 {} 存在多個數據,將自動合併使用 {} SSH的配置信息
@@ -335,6 +343,7 @@ i18n.no_management_permission.fd25=您沒有對應管理權限\:-2
 i18n.workspace_env_vars.f7e8=工作空間環境變量
 i18n.unsupported_mode.a3d3=暫不支持的模式:
 i18n.select_node_and_project.6021=請選擇節點和項目
+i18n.initialize_sql.6691=執行初始化SQL文件
 i18n.operation_succeeded.3313=操作成功
 i18n.url_length_exceeded.ca1c=url 長度不能超過 200
 i18n.waiting_to_close_process.3634=等待關閉[Process]進程:{}
@@ -364,6 +373,7 @@ i18n.webhooks_invocation_error.9792=WebHooks 調用錯誤
 i18n.missing_parent_id.4331=父任務id缺失
 i18n.data_modification_time_format_incorrect.7ffe=數據修改時間格式不正確 {} {}
 i18n.cannot_delete_recent_logs.ee19=不能刪除近一天相關的日誌(文件修改時間)
+i18n.monitor_name.9aff=監控
 i18n.agent_jar_not_exist.28ac=Agent JAR包不存在
 i18n.parse_certificate_unknown_error.c43c=解析證書發生未知錯誤:
 i18n.node_info_incomplete.3b69=對應的節點信息不完整不能繼續
@@ -377,6 +387,7 @@ i18n.system_logs.84aa=系統日誌
 i18n.machines_ssh_data_fixed.1387=成功修復 {} 條機器 SSH 數據
 i18n.docker_label_required.b690=請填要執行 docker 標籤
 i18n.no_matching_permission.09cf=未匹配到合適的權限不足
+i18n.ftp_create_folder_exception.a4fe=FTP創建文件夾異常
 i18n.data_id_does_not_exist.a566=數據id 不存在
 i18n.no_ssh_info.a8ec=沒有對應 SSH 信息
 i18n.distribution_with_build_items_message.45f5=當前分發存在構建項,不能直接刪除(需要提前解綁或者刪除關聯數據後才能刪除)
@@ -401,6 +412,7 @@ i18n.node_not_enabled.10ef={} 節點未啟用
 i18n.client_id_not_configured.ab8e=沒有配置 clientId
 i18n.build_product_dir_not_empty.ba06=構建產物目錄不能為空,長度1-200
 i18n.trigger_token_error_or_expired.8976=觸發token錯誤,或者已經失效
+i18n.ftp_connection_failed.1f2f=連接FTP失敗
 i18n.auth_info_error.c184=授權信息錯誤
 i18n.download_file_error.5bcd=下載文件異常\:
 i18n.rollback_ended.fb1d=執行回滾結束:{}
@@ -416,6 +428,7 @@ i18n.id_already_exists.6208=id已經存在啦
 i18n.ssh_info.ebe6=SSH 信息
 i18n.current_status.81c0=\ 當前還在:
 i18n.project_path_no_spaces.263c=項目路徑不能包含空格
+i18n.ftp_asset_management.c6a5=FTP資產管理
 i18n.please_fill_in_address_of.9e02=請填寫 %s 的 地址
 i18n.rsa_private_key_file_invalid.5f12=rsa 私鑰文件不存在或者有誤
 i18n.ssh_node_required.4566=請選擇 ssh 節點
@@ -456,6 +469,7 @@ i18n.load_plugin.1f64=加載:{} 插件
 i18n.host_field_required.5c36=第 {} 行 host 字段不能位空
 i18n.container_build_interrupted.a17b=容器 build 被中斷\:
 i18n.upload_action.d5a7=上傳
+i18n.ftp_connection_or_operation_exception.09af=FTP 連接或操作異常
 i18n.delete_file_failure.041f=刪除文件失敗,請檢查
 i18n.no_cluster_info_found.fb40=沒有找到對應的集羣信息
 i18n.script_template_log.30cb=腳本模板日誌
@@ -509,6 +523,7 @@ i18n.invalid_zip_file.3092=上傳的壓縮包不是 Jpom [{}] 包
 i18n.publish_command_non_zero_exit_code.ea80=執行發佈命令退出碼非0,{}
 i18n.project_path_promotion_issue.2250=項目路徑存在提升目錄問題
 i18n.handle_node_deletion_script_failure_duplicate.821e=處理 {} 節點刪除腳本失敗{}
+i18n.ftp_folder_query_failed.0011=無法查詢文件夾FTP,
 i18n.no_matching_process_type.b468=未匹配到合適的處理類型
 i18n.unsupported_type.7495=不支持的類型
 i18n.submit_task_queue_success.5f5b=提交任務隊列成功,當前隊列數:
@@ -567,6 +582,7 @@ i18n.select_correct_pre_publish_script.d230=請選擇正確的發佈前腳本
 i18n.greeting.5ecd=您好,Jpom
 i18n.ssh_connection_failed.4719=ssh連接失敗
 i18n.oauth2_redirect_failed.6dcd=跳轉 oauth2 失敗,{} {}
+i18n.ftp_upload_failed.8298=FTP 上傳失敗
 i18n.data_workspace_mismatch.ae1d=數據工作空間和操作工作空間不一致
 i18n.process_file_event_exception.e8e6=處理文件事件異常
 i18n.current_docker_cluster_has_no_management_nodes_online.56cd=當前 {} docker 集羣沒有管理節點在線
@@ -578,6 +594,7 @@ i18n.restart_operation.5e3a=執行重啟操作
 i18n.no_h2_data_info_for_migration.5799=沒有 h2 數據信息不用遷移
 i18n.publish_success.2fff=發佈成功
 i18n.system_cache.c4a8=系統緩存
+i18n.no_ftp_item.8e39=沒有對應的ftp項
 i18n.distribution_machine_required.5921=請選擇分發的機器
 i18n.build_call_container_exception.6e04=構建調用容器異常
 i18n.process_killed_successfully.a4c3=成功kill
@@ -586,6 +603,7 @@ i18n.auto_clear_data_errors.112f=自動清除數據錯誤 {} {}
 i18n.publish_directory_is_empty.79c6=發佈目錄為空
 i18n.file_or_directory_not_found.f03e=文件不存在或者是目錄\:
 i18n.clear_file_cache_failed.5cd1=清空文件緩存失敗
+i18n.database_backup_complete_path.861b=數據庫備份完成,保存路徑為
 i18n.file_downloading_status.c995=文件下載中:
 i18n.system_IP_authorization.9c08=系統配置IP授權
 i18n.node_failed.20d5=節點失敗:
@@ -613,6 +631,7 @@ i18n.delete_failure_with_colon_and_full_stop.bc42=刪除失敗:
 i18n.product_directory_cannot_skip_levels.3ad4=產物目錄不能越級:
 i18n.fix_null_workspace_data.4d0b=修復工作空間為 null 的數據 {} {}
 i18n.external_config_file_path.06ec=外部配置文件路徑
+i18n.asset_ftp_info.3b75=資產FTP信息
 i18n.soft_link_project_department_exists.fa97=軟鏈的項目部存在
 i18n.docker_info.00d2=docker 信息
 i18n.log_file_does_not_exist.f6c6=日誌文件不存在
@@ -621,6 +640,7 @@ i18n.no_file_info.db01=沒有對應的文件信息
 i18n.record_operation_log_exception.8012=記錄操作日誌異常
 i18n.corresponding_file_required.57b3=請選擇對應到文件
 i18n.build_command_no_delete.df52=構建命令不能包含刪除命令
+i18n.ftp_info_table.b177=ftp信息表
 i18n.file_merge_error.f32f=文件合併後異常,文件不完整可能被損壞
 i18n.script_info_not_found.bd8d=找不到對應的腳本信息
 i18n.cannot_modify_own_info.4036=不能修改自己的信息
@@ -675,6 +695,7 @@ i18n.container_build_host_config_conversion_failure.27aa=容器構建 hostConfig
 i18n.build_task_count_and_queue_count.f0b6=當前構建中任務數:{},隊列中任務數:{} {}
 i18n.node_cache.d68c=節點緩存
 i18n.cluster_node_not_in_system.0645=當前集羣對應的節點,不在本系統中無法退出集羣
+i18n.ftp_selection.c903=請選擇分發FTP項
 i18n.file_search_failed.231b=文件搜索失敗
 i18n.exported_ssh_data.ce88=導出的 ssh 數據
 i18n.execution_ended_with_duration.a59b=執行結束 {}流程,耗時:{}
@@ -703,6 +724,7 @@ i18n.ssh_batch_command_execution_exception.029a=ssh 批量執行命令異常
 i18n.content_type_not_supported.81a9=不支持的 contentType
 i18n.cloud_server_network_issues.a865=雲服務器的安全組配置等網絡相關問題排查定位。
 i18n.start_building_image.eacd={} 開始構建鏡像 {}{}
+i18n.ftp_already_exists.d66b=對應的FTP已經存在啦
 i18n.configure_correct_redirect_url.058e=請配置正確的重定向 url
 i18n.no_cache_info_with_minus_one.52f2=沒有對應的緩存信息:-1
 i18n.no_node_entry_found.b1ef=沒有找到對應的節點項:{}
@@ -714,6 +736,7 @@ i18n.no_docker_info_found.6d38=沒有找到對應的 docker 信息
 i18n.update_docker_machine_id_failed.063d=更新 DOCKER 表機器id 失敗:
 i18n.build_status_abnormal.8ca1=構建狀態異常或者被取消
 i18n.node_connection_failure.896d=節點連接失敗,請檢查節點是否在線
+i18n.database_load_success_url.5f64=數據庫加載成功,URL為
 i18n.login_name_already_exists.2511=登錄名已經存在
 i18n.port_error.312e=端口錯誤
 i18n.account_disabled.9361=賬號已經被禁用,不能使用
@@ -763,9 +786,12 @@ i18n.error_sql.15ff=錯誤 sql\:{}
 i18n.verification_method_not_configured.7358={}未配置驗證方法:{}
 i18n.local_git_certificate_not_supported.b395=暫時不支持本地 git 指定證書拉取代碼
 i18n.unsupported_method_with_colon.eae8=不支持的方式:
+i18n.exported_ftp_data.2b54=導出的 ftp 數據
 i18n.current_docker_has_no_cluster_info.0b52=當前 docker 沒有集羣信息
 i18n.cannot_delete_self.fec9=不能刪除自己
 i18n.operation_monitoring_error.8036=執行操作監控錯誤
+i18n.ftp_read_file_failed.e738=FTP 讀取文件失敗
+i18n.file_exists.145b=文件夾或文件已經存在
 i18n.alias_or_token_error.d5c6=別名或者token錯誤,或者已經失效
 i18n.version_config_info.7b29=版本配置信息
 i18n.delete_old_package.ca95=刪除舊程序包:{}
@@ -778,6 +804,7 @@ i18n.node_service_stopped_failed_restart.4307=【{}】節點的【{}】項目{}
 i18n.associated_workspace.885b=所屬工作空間
 i18n.auto_clear_machine_node_stats_logs.5279=自動清理 {} 條機器節點統計日誌
 i18n.unsupported_mode.501d=不支持的模式
+i18n.ftp_client_build_failure.aa55=構建 FTP 客户端失敗 [{}]\: {}
 i18n.missing_script_message.af89=找不到對應的腳本
 i18n.start_migrating.20d6=開始遷移 {} {}
 i18n.incompatible_database_version.8f7b=數據庫版本不兼容,需要處理跨版本升級。
@@ -802,6 +829,7 @@ i18n.configure_user_notification.250d=請配置用户通知
 i18n.operation_status_code.8231=操作狀態碼
 i18n.agent_jar_damaged.74a8=Agent JAR 損壞請重新上傳,
 i18n.read_error.7fa5=讀取錯誤
+i18n.import_success.ef46=導入成功:{}
 i18n.network_resource_monitoring_error.4ede=網卡資源監控異常:
 i18n.select_workspace_to_modify.ac87=請選擇要修改的工作空間
 i18n.decode_failure.822e=解碼失敗
@@ -832,6 +860,8 @@ i18n.project_id_not_found.b87e=沒有項目id
 i18n.operation_type.de9c=操作類型
 i18n.project_has_logs_cannot_migrate.2e0e=當前項目存在日誌閲讀,不能直接遷移
 i18n.delete_failure_with_colon.b429=刪除失敗\:
+i18n.no_ftp_server_correspondence.1af7=沒有對應的Ftp
+i18n.ftp_import_template.8fa3=ftp導入模板
 i18n.tag_cannot_contain_colon.f9ae=標籤不能包含 :
 i18n.distribute_id_already_exists_globally.6478=分發id已經存在啦,分發id需要全局唯一
 i18n.import_exception.04b6=導入第 {} 條數據異常\:{}
@@ -895,7 +925,9 @@ i18n.no_manager_node_found.5934=當前集羣未找到任何管理節點
 i18n.file_transfer_exception.bda6=轉發文件異常
 i18n.select_node.f8a6=請選擇節點
 i18n.upload_progress_with_units.44ad=上傳文件進度\:{} {}/{} {}
+i18n.ftp_download_file_failed.2e42=FTP 下載文件失敗
 i18n.ssh_terminal_execution_log.58f1=ssh 終端執行日誌
+i18n.ftp_file_manager.c52e=FTP文件管理
 i18n.project_monitor.d2ff=項目監控
 i18n.push_image_interrupted.6377=push image 被中斷\:
 i18n.default_value.1aa9={} [默認]
@@ -986,6 +1018,7 @@ i18n.certificate_already_exists.adf9=當前證書已經存在啦(系統全局範
 i18n.oshi_file_system_monitoring_exception.dc24=oshi 文件系統資源監控異常
 i18n.image_cannot_be_empty.1600=鏡像不能為空
 i18n.no_corresponding_node_info.cd24=沒有對應到節點信息
+i18n.auto_backup_path.a16b=自動備份數據文件到路徑
 i18n.node_plugin_version_required.2318=node 插件 version 不能為空
 i18n.start_new_thread_for_h2_database_backup.9337=啟動一個新線程來執行 H2 數據庫備份...啟動
 i18n.docker_exec_terminal_process_ended.c734=[{}] docker exec 終端進程結束
@@ -1005,6 +1038,7 @@ i18n.protocol_not_supported.b906=不支持的 protocol
 i18n.force_unbind_succeeded.5bfd=強制解綁成功
 i18n.unexpected_exception.2b52=發生異常
 i18n.check_email_error.636c=檢查郵箱信息錯誤:{}
+i18n.ftp_unauthorized_directory.df73=此ftp未授權操作此目錄
 i18n.initialization_success.4725=初始化成功
 i18n.running_status.d679=運行中
 i18n.auto_clean_temp_dir.11d2=自動清理臨時目錄
@@ -1101,6 +1135,7 @@ i18n.batch_trigger_script_exception.8fb4=服務端腳本批量觸發異常
 i18n.upgrade_failure.4ae2=升級失敗
 i18n.no_branch_or_tag_message.8ae3=沒有 {} 分支/標籤
 i18n.general_error_message.728a=啊哦,好像哪裏出錯了,請稍候再試試吧~
+i18n.ftp_connection_failure.0f31=關閉 FTP 連接失敗
 i18n.service_info_incomplete_with_code2.e9ca=服務信息不完整不能操作:-2
 i18n.ignored_operation.edee=忽略的操作:{}
 i18n.monitor_name_cannot_be_empty.514a=監控名稱不能為空
@@ -1115,6 +1150,7 @@ i18n.select_related_data_id.7fba=請選擇關聯數據 ID
 i18n.node_name_required.5bdf=請填寫節點名稱
 i18n.client_terminated_connection.6886=客户端終止連接:{}
 i18n.close_thread_pool.4cd9=關閉 {} 線程池
+i18n.ftp_upload_file_exception.118c=FTP上傳文件異常
 i18n.clear_ip_whitelist_config_success.8cf6=清除 IP 白名單配置成功
 i18n.no_corresponding_folder.621f=沒有對應文件夾
 i18n.no_online_manager_node_found.05d7=當前集羣未找到在線的管理節點
@@ -1124,6 +1160,7 @@ i18n.execution_completed.24a1=執行完畢\:
 i18n.close_project_failure.a1d2=關閉項目失敗:
 i18n.select_folder_to_compress.915f=請選擇文件夾進行壓縮
 i18n.config_file_not_found.fc87=均未找到配置文件
+i18n.ftp_item_not_found.60a7=沒有找到對應的ftp項:{}
 i18n.update_ssh_machine_id_failed.bd24=更新 SSH 表機器id 失敗:
 i18n.select_pull_code_protocol.fc24=請選擇拉取代碼的協議
 i18n.auth_directory_cannot_be_empty.21ba=授權目錄不能為空
@@ -1291,6 +1328,7 @@ i18n.program_already_running.96e1=當前程序正在運行中,不能重複啟
 i18n.password_change_success.8013=修改密碼成功!
 i18n.build_resource_cleanup_failed.c4cf=清理構建資源失敗
 i18n.incorrect_publish_method.e095=發佈方法不正確
+i18n.ftp_directory.a790=請輸入發佈到ftp中的目錄
 i18n.import_low_version_data_to_new_version.247b=2. 將導出的低版本數據( sql 文件) 導入到新版本中【啟動程序參數裏面添加 --replace-import-h2-sql\=/xxxx.sql (路徑需要替換為第一步控制枱輸出的 sql 文件保存路徑)】
 i18n.incorrect_line_number.5877=行號不正確
 i18n.file_deletion_event_with_details.7537=文件刪除事件:{} {}
@@ -1299,6 +1337,7 @@ i18n.maven_plugin_version_required.71f1=maven 插件 version 不能為空
 i18n.trigger_token_error_or_expired_with_code.393b=觸發token錯誤,或者已經失效\:-1
 i18n.invalid_webhooks_address.d836=WebHooks 地址不合法
 i18n.project_path_auth_not_under_jpom.0e18=項目路徑授權不能位於Jpom目錄下
+i18n.ftp_rename_failed_exception.0fcc=FTP重命名失敗異常
 i18n.reconnect_failure.7c01=重連失敗
 i18n.project_operations.03d9=項目運維
 i18n.image_not_exist.ee17=鏡像不存在
@@ -1338,6 +1377,7 @@ i18n.refreshing_cache.c969=正在刷新緩存中,請勿重複刷新
 i18n.start_executing_database_event.fc57=開始執行數據庫事件:{}
 i18n.static_file_storage.35f6=靜態文件存儲
 i18n.project_log_is_existing_folder.a80a=項目log是一個已經存在的文件夾
+i18n.check_ftp_connection_failed.f7de=檢測 FTP 連接失效 [{}],準備重建\: {}
 i18n.project_data_workspace_id_inconsistency.7ed6=項目數據工作空間ID[{}]查詢出節點ID不一致, 舊數據\: {}, 新數據\: {}
 i18n.query_workspace_error.6a0d=查詢錯誤的工作空間失敗
 i18n.unknown_database_mode.f9e5=當前數據庫模式未知
@@ -1375,8 +1415,8 @@ i18n.token_invalid_or_expired.cb96=token錯誤,或者已經失效\:-1
 i18n.list_and_query.c783=列表、查詢
 i18n.migration_docker_cert_error.a5ea=遷移 docker[{}] 證書發生異常
 i18n.file_downloading.7a8f=文件下載中
-i18n.mark_must_contain_letters_numbers_underscores.667d=標記只能包含字母、數字、下劃線
 i18n.please_fill_in_runs_on.c2ff=請填寫runsOn。
+i18n.mark_must_contain_letters_numbers_underscores.667d=標記只能包含字母、數字、下劃線
 i18n.create_build_task_exception.06f1=創建構建任務異常
 i18n.execution_interrupted.1bb6=執行被中斷
 i18n.event_loss_or_execution_error.7b14=事件丟失或者執行錯誤:{} {}
@@ -1401,6 +1441,7 @@ i18n.please_pass_parameter.3182=請傳入參數
 i18n.node_no_data_pulled.0dae={} 節點沒有拉取到任何 {},但是刪除了數據:{}
 i18n.node_info_not_found.2c8c=沒有查詢到節點信息:
 i18n.please_fill_in_repository_address.0cf8=請填寫倉庫地址
+i18n.no_ftp_correspondence.23c4=沒有對應的FTP
 i18n.table_without_primary_key.7392=表沒有主鍵
 i18n.invalid_shard_id.46fd=不合法的分片id
 i18n.execution_ended.b793=執行結束\:{}
@@ -1499,6 +1540,7 @@ i18n.config_file_database_config_not_parsed.47b2=未解析出配置文件中的
 i18n.data_already_exists.0397=導入的數據已經存在啦
 i18n.please_check_in_time.3b4f=請及時檢查
 i18n.installation_success.811f=安裝成功
+i18n.timeout.e944=超時
 i18n.cluster_management.74ea=集羣管理
 i18n.type_field_value_error.14cf=第 {} 行 type 字段值錯誤(Git/Svn)
 i18n.migrate_data.f556=遷移數據
@@ -1514,6 +1556,7 @@ i18n.backup_data_trigger.a71a=備份數據觸發器
 i18n.file_signature_info_not_found.83bf=沒有文件簽名信息
 i18n.associated_nodes_warning.64d8=當前機器還關聯{}個節點,不能直接刪除(需要提前解綁或者刪除關聯數據後才能刪除)
 i18n.build_unknown_error.dad6=構建發生未知錯誤
+i18n.start_import_data.ea31=開始導入數據:{}
 i18n.new_version_exists_download_unavailable.4ba7=存在新版本,下載地址不可用
 i18n.compare_backup_failure.303e=對比清空項目文件備份失敗
 i18n.upload_success_and_restart.7bc3=上傳成功並重啟
@@ -1547,9 +1590,11 @@ i18n.download_exception.e616=下載文件異常
 i18n.process_file_deletion_exception.1c6e=處理文件刪除異常
 i18n.trigger_auto_execute_command_template_exception.4e01=觸發自動執行命令模版異常
 i18n.no_build.d163=沒有對應的構建
+i18n.ftp_management.cb91=FTP管理
 i18n.start_executing_publishing_with_file_size.5039=開始執行發佈,需要發佈的文件大小:{}
 i18n.command_execution_exception.4ccd=執行命令異常
 i18n.operation_ip.cbd4=操作IP
+i18n.exception.c195=異常
 i18n.auth_directory_cannot_contain_hierarchy.d6ca=授權目錄中不能存在包含關係:
 i18n.ip_authorization_interception_exception.8130=IP授權攔截異常,請檢查配置是否正確
 i18n.refresh_token_timeout.3291=刷新token超時

+ 47 - 2
modules/common/src/main/resources/i18n/messages_zh_TW.properties

@@ -1,5 +1,5 @@
 #i18n zh_TW
-#Thu Jan 09 16:59:43 CST 2025
+#Thu Jun 12 16:41:17 CST 2025
 i18n.ssh_info_does_not_exist.5ed0=ssh 資訊不存在啦
 i18n.incompatible_program_versions.5291=當前程式版本 {} 新版程式最低相容 {} 不能直接升級
 i18n.no_projects_configured.e873=沒有配置任何專案
@@ -8,6 +8,7 @@ i18n.machine_ssh_info.8dbb=機器SSH資訊
 i18n.machine_info_not_exist.3468=對應的機器資訊不存在
 i18n.unsupported_method.a1de=不支援的方式
 i18n.delete_local_image_failed.91fa=刪除本地映象失敗
+i18n.cluster_not_grouped.8f54=當前叢集還未繫結分組,不能監控 FTP 資產資訊
 i18n.service_name_in_cluster_required.5446=請填寫叢集中的服務名
 i18n.cluster_manager_node_not_found.1cd0=沒有找到叢集管理節點
 i18n.distribute_id_already_exists.2168=分發id已經存在啦
@@ -34,6 +35,7 @@ i18n.start_waiting_for_data_migration.e76f=開始等待資料遷移
 i18n.empty_file_or_folder_for_publish.cae8=釋出的檔案或者資料夾為空,不能繼續釋出
 i18n.demo_account_cannot_use_feature.a1a1=演示賬號不能使用該功能
 i18n.repository_type_required.9414=請選擇倉庫型別
+i18n.start_backup_database.e554=開始備份資料庫
 i18n.async_resource_expired.2ddc=非同步資源過期,需要主動關閉,{} {}
 i18n.mark_cannot_be_empty.1927=標記不能為空
 i18n.get_success.fb55=獲取成功
@@ -50,9 +52,11 @@ i18n.upload_failed_no_matching_project.b219=上傳失敗,沒有找到對應的
 i18n.no_branches_or_tags_in_repository.76b6=倉庫沒有任何分支或者標籤
 i18n.download_file_description.10cb=下載檔案 {} {} {}
 i18n.delay_build.7d62=延遲 {} 秒後開始構建
+i18n.ftp_not_exist.f9b3=不存在對應ftp
 i18n.default_cluster.38cf=預設叢集
 i18n.node_and_check_project_failed.ac4b=節點與檢查專案失敗
 i18n.load_oauth2_config.da42=載入 oauth2 配置 :{} {}
+i18n.affected_rows.5781=影響行數
 i18n.cluster_not_bound_to_group_for_node_monitoring.1586=當前叢集還未繫結分組,不能監控叢集節點資產資訊
 i18n.cannot_execute_error.4c29=不能執行:error
 i18n.no_environment_variable.c79f=沒有對應的環境變數
@@ -114,6 +118,7 @@ i18n.no_ssh_entry_found.d0e1=沒有找到對應的ssh項:{}
 i18n.build_runs_on_image_interrupted.00fd=構建 runsOn 映象被中斷
 i18n.incorrect_parameter_format.9efb=傳入的引數格式不正確
 i18n.multiple_certificate_files_found.bee3=找到 2 個以上的證書檔案
+i18n.file_not_exist_or_unable_to_download.b977=檔案不存在或無法下載\:
 i18n.invalid_runs_on_image_name.4b96=runsOn 映象名稱不合法
 i18n.dockerfile_path_required.69ac=請填寫要執行的 Dockerfile 路徑
 i18n.file_upload_mode_not_configured.b3b2=沒有配置檔案上傳模式
@@ -248,6 +253,7 @@ i18n.select_node_to_modify.6617=請選擇要修改的節
 i18n.correct_dingtalk_address_required.2b4a=請輸入正確釘釘地址
 i18n.query_folder_sftp_failed.9d35=查詢資料夾 SFTP 失敗,
 i18n.name_field_required.e0c5=第 {} 行 name 欄位不能位空
+i18n.ftp_connection_failed_message.bd99=連線FTP失敗:
 i18n.plugin_end_log_connection_successful.9035=連線成功:外掛端日誌
 i18n.start_building_with_number_and_path.c41c=開始構建 \#{} 構建執行路徑 \: {}
 i18n.start_executing_upload_pre_command.fb5c=開始執行上傳前命令
@@ -292,6 +298,7 @@ i18n.login_name_cannot_contain_chinese_and_special_characters.48a8=登入名不
 i18n.cannot_join_cluster_as_role.01d4=不能以 {} 角色加入叢集
 i18n.not_an_enumeration.8244=不是列舉
 i18n.script_not_exist.b180=對應指令碼已經不存在啦
+i18n.file_not_exist_or_unable_to_open.b045=檔案不存在或無法開啟\:
 i18n.already_offline.d3b5=已經離線啦
 i18n.rebuild_success.5938=重建成功
 i18n.hours.2de0=小時
@@ -328,6 +335,7 @@ i18n.upload_progress_template.ac3f=上傳檔案進度\:{}/{} {}
 i18n.mark_already_exists.0ccc=標記已存在
 i18n.docker_not_found.2a2e=\ 沒有找到任何 docker。可能docker tag 填寫不正確,需要為 docker 配置標籤
 i18n.download_remote_file_failed.fcc3=下載遠端檔案失敗\:
+i18n.no_matching_asset_ftp.d420=不存在對應的資產FTP
 i18n.no_file_found.6f1b=沒有找到 {} 檔案
 i18n.distribute_result.a230=分發結果:{}
 i18n.multiple_ssh_addresses_found.b3f7=SSH 地址 {} 存在多個資料,將自動合併使用 {} SSH的配置資訊
@@ -335,6 +343,7 @@ i18n.no_management_permission.fd25=您沒有對應管理許可權\:-2
 i18n.workspace_env_vars.f7e8=工作空間環境變數
 i18n.unsupported_mode.a3d3=暫不支援的模式:
 i18n.select_node_and_project.6021=請選擇節點和專案
+i18n.initialize_sql.6691=執行初始化SQL檔案
 i18n.operation_succeeded.3313=操作成功
 i18n.url_length_exceeded.ca1c=url 長度不能超過 200
 i18n.waiting_to_close_process.3634=等待關閉[Process]程序:{}
@@ -364,6 +373,7 @@ i18n.webhooks_invocation_error.9792=WebHooks 呼叫錯誤
 i18n.missing_parent_id.4331=父任務id缺失
 i18n.data_modification_time_format_incorrect.7ffe=資料修改時間格式不正確 {} {}
 i18n.cannot_delete_recent_logs.ee19=不能刪除近一天相關的日誌(檔案修改時間)
+i18n.monitor_name.9aff=監控
 i18n.agent_jar_not_exist.28ac=Agent JAR包不存在
 i18n.parse_certificate_unknown_error.c43c=解析證書發生未知錯誤:
 i18n.node_info_incomplete.3b69=對應的節點資訊不完整不能繼續
@@ -377,6 +387,7 @@ i18n.system_logs.84aa=系統日誌
 i18n.machines_ssh_data_fixed.1387=成功修復 {} 條機器 SSH 資料
 i18n.docker_label_required.b690=請填要執行 docker 標籤
 i18n.no_matching_permission.09cf=未匹配到合適的許可權不足
+i18n.ftp_create_folder_exception.a4fe=FTP建立資料夾異常
 i18n.data_id_does_not_exist.a566=資料id 不存在
 i18n.no_ssh_info.a8ec=沒有對應 SSH 資訊
 i18n.distribution_with_build_items_message.45f5=當前分發存在構建項,不能直接刪除(需要提前解綁或者刪除關聯資料後才能刪除)
@@ -401,6 +412,7 @@ i18n.node_not_enabled.10ef={} 節點未啟用
 i18n.client_id_not_configured.ab8e=沒有配置 clientId
 i18n.build_product_dir_not_empty.ba06=構建產物目錄不能為空,長度1-200
 i18n.trigger_token_error_or_expired.8976=觸發token錯誤,或者已經失效
+i18n.ftp_connection_failed.1f2f=連線FTP失敗
 i18n.auth_info_error.c184=授權資訊錯誤
 i18n.download_file_error.5bcd=下載檔案異常\:
 i18n.rollback_ended.fb1d=執行回滾結束:{}
@@ -416,6 +428,7 @@ i18n.id_already_exists.6208=id已經存在啦
 i18n.ssh_info.ebe6=SSH 資訊
 i18n.current_status.81c0=\ 當前還在:
 i18n.project_path_no_spaces.263c=專案路徑不能包含空格
+i18n.ftp_asset_management.c6a5=FTP資產管理
 i18n.please_fill_in_address_of.9e02=請填寫 %s 的 地址
 i18n.rsa_private_key_file_invalid.5f12=rsa 私鑰檔案不存在或者有誤
 i18n.ssh_node_required.4566=請選擇 ssh 節點
@@ -456,6 +469,7 @@ i18n.load_plugin.1f64=載入:{} 外掛
 i18n.host_field_required.5c36=第 {} 行 host 欄位不能位空
 i18n.container_build_interrupted.a17b=容器 build 被中斷\:
 i18n.upload_action.d5a7=上傳
+i18n.ftp_connection_or_operation_exception.09af=FTP 連線或操作異常
 i18n.delete_file_failure.041f=刪除檔案失敗,請檢查
 i18n.no_cluster_info_found.fb40=沒有找到對應的叢集資訊
 i18n.script_template_log.30cb=指令碼模板日誌
@@ -509,6 +523,7 @@ i18n.invalid_zip_file.3092=上傳的壓縮包不是 Jpom [{}] 包
 i18n.publish_command_non_zero_exit_code.ea80=執行釋出命令退出碼非0,{}
 i18n.project_path_promotion_issue.2250=專案路徑存在提升目錄問題
 i18n.handle_node_deletion_script_failure_duplicate.821e=處理 {} 節點刪除指令碼失敗{}
+i18n.ftp_folder_query_failed.0011=無法查詢資料夾FTP,
 i18n.no_matching_process_type.b468=未匹配到合適的處理型別
 i18n.unsupported_type.7495=不支援的型別
 i18n.submit_task_queue_success.5f5b=提交任務佇列成功,當前佇列數:
@@ -567,6 +582,7 @@ i18n.select_correct_pre_publish_script.d230=請選擇正確的釋出前指令碼
 i18n.greeting.5ecd=您好,Jpom
 i18n.ssh_connection_failed.4719=ssh連線失敗
 i18n.oauth2_redirect_failed.6dcd=跳轉 oauth2 失敗,{} {}
+i18n.ftp_upload_failed.8298=FTP 上傳失敗
 i18n.data_workspace_mismatch.ae1d=資料工作空間和操作工作空間不一致
 i18n.process_file_event_exception.e8e6=處理檔案事件異常
 i18n.current_docker_cluster_has_no_management_nodes_online.56cd=當前 {} docker 叢集沒有管理節點線上
@@ -578,6 +594,7 @@ i18n.restart_operation.5e3a=執行重啟操作
 i18n.no_h2_data_info_for_migration.5799=沒有 h2 資料資訊不用遷移
 i18n.publish_success.2fff=釋出成功
 i18n.system_cache.c4a8=系統快取
+i18n.no_ftp_item.8e39=沒有對應的ftp項
 i18n.distribution_machine_required.5921=請選擇分發的機器
 i18n.build_call_container_exception.6e04=構建呼叫容器異常
 i18n.process_killed_successfully.a4c3=成功kill
@@ -586,6 +603,7 @@ i18n.auto_clear_data_errors.112f=自動清除資料錯誤 {} {}
 i18n.publish_directory_is_empty.79c6=釋出目錄為空
 i18n.file_or_directory_not_found.f03e=檔案不存在或者是目錄\:
 i18n.clear_file_cache_failed.5cd1=清空檔案快取失敗
+i18n.database_backup_complete_path.861b=資料庫備份完成,儲存路徑為
 i18n.file_downloading_status.c995=檔案下載中:
 i18n.system_IP_authorization.9c08=系統配置IP授權
 i18n.node_failed.20d5=節點失敗:
@@ -613,6 +631,7 @@ i18n.delete_failure_with_colon_and_full_stop.bc42=刪除失敗:
 i18n.product_directory_cannot_skip_levels.3ad4=產物目錄不能越級:
 i18n.fix_null_workspace_data.4d0b=修復工作空間為 null 的資料 {} {}
 i18n.external_config_file_path.06ec=外部配置檔案路徑
+i18n.asset_ftp_info.3b75=資產FTP資訊
 i18n.soft_link_project_department_exists.fa97=軟鏈的專案部存在
 i18n.docker_info.00d2=docker 資訊
 i18n.log_file_does_not_exist.f6c6=日誌檔案不存在
@@ -621,6 +640,7 @@ i18n.no_file_info.db01=沒有對應的檔案資訊
 i18n.record_operation_log_exception.8012=記錄操作日誌異常
 i18n.corresponding_file_required.57b3=請選擇對應到檔案
 i18n.build_command_no_delete.df52=構建命令不能包含刪除命令
+i18n.ftp_info_table.b177=ftp資訊表
 i18n.file_merge_error.f32f=檔案合併後異常,檔案不完整可能被損壞
 i18n.script_info_not_found.bd8d=找不到對應的指令碼資訊
 i18n.cannot_modify_own_info.4036=不能修改自己的資訊
@@ -675,6 +695,7 @@ i18n.container_build_host_config_conversion_failure.27aa=容器構建 hostConfig
 i18n.build_task_count_and_queue_count.f0b6=當前構建中任務數:{},佇列中任務數:{} {}
 i18n.node_cache.d68c=節點快取
 i18n.cluster_node_not_in_system.0645=當前叢集對應的節點,不在本系統中無法退出叢集
+i18n.ftp_selection.c903=請選擇分發FTP項
 i18n.file_search_failed.231b=檔案搜尋失敗
 i18n.exported_ssh_data.ce88=匯出的 ssh 資料
 i18n.execution_ended_with_duration.a59b=執行結束 {}流程,耗時:{}
@@ -703,6 +724,7 @@ i18n.ssh_batch_command_execution_exception.029a=ssh 批量執行命令異常
 i18n.content_type_not_supported.81a9=不支援的 contentType
 i18n.cloud_server_network_issues.a865=雲伺服器的安全組配置等網路相關問題排查定位。
 i18n.start_building_image.eacd={} 開始構建映象 {}{}
+i18n.ftp_already_exists.d66b=對應的FTP已經存在啦
 i18n.configure_correct_redirect_url.058e=請配置正確的重定向 url
 i18n.no_cache_info_with_minus_one.52f2=沒有對應的快取資訊:-1
 i18n.no_node_entry_found.b1ef=沒有找到對應的節點項:{}
@@ -714,6 +736,7 @@ i18n.no_docker_info_found.6d38=沒有找到對應的 docker 資訊
 i18n.update_docker_machine_id_failed.063d=更新 DOCKER 表機器id 失敗:
 i18n.build_status_abnormal.8ca1=構建狀態異常或者被取消
 i18n.node_connection_failure.896d=節點連線失敗,請檢查節點是否線上
+i18n.database_load_success_url.5f64=資料庫載入成功,URL為
 i18n.login_name_already_exists.2511=登入名已經存在
 i18n.port_error.312e=埠錯誤
 i18n.account_disabled.9361=賬號已經被禁用,不能使用
@@ -763,9 +786,12 @@ i18n.error_sql.15ff=錯誤 sql\:{}
 i18n.verification_method_not_configured.7358={}未配置驗證方法:{}
 i18n.local_git_certificate_not_supported.b395=暫時不支援本地 git 指定證書拉取程式碼
 i18n.unsupported_method_with_colon.eae8=不支援的方式:
+i18n.exported_ftp_data.2b54=匯出的 ftp 資料
 i18n.current_docker_has_no_cluster_info.0b52=當前 docker 沒有叢集資訊
 i18n.cannot_delete_self.fec9=不能刪除自己
 i18n.operation_monitoring_error.8036=執行操作監控錯誤
+i18n.ftp_read_file_failed.e738=FTP 讀取檔案失敗
+i18n.file_exists.145b=資料夾或檔案已經存在
 i18n.alias_or_token_error.d5c6=別名或者token錯誤,或者已經失效
 i18n.version_config_info.7b29=版本配置資訊
 i18n.delete_old_package.ca95=刪除舊程式包:{}
@@ -778,6 +804,7 @@ i18n.node_service_stopped_failed_restart.4307=【{}】節點的【{}】專案{}
 i18n.associated_workspace.885b=所屬工作空間
 i18n.auto_clear_machine_node_stats_logs.5279=自動清理 {} 條機器節點統計日誌
 i18n.unsupported_mode.501d=不支援的模式
+i18n.ftp_client_build_failure.aa55=構建 FTP 客戶端失敗 [{}]\: {}
 i18n.missing_script_message.af89=找不到對應的指令碼
 i18n.start_migrating.20d6=開始遷移 {} {}
 i18n.incompatible_database_version.8f7b=資料庫版本不相容,需要處理跨版本升級。
@@ -802,6 +829,7 @@ i18n.configure_user_notification.250d=請配置使用者通知
 i18n.operation_status_code.8231=操作狀態碼
 i18n.agent_jar_damaged.74a8=Agent JAR 損壞請重新上傳,
 i18n.read_error.7fa5=讀取錯誤
+i18n.import_success.ef46=匯入成功:{}
 i18n.network_resource_monitoring_error.4ede=網絡卡資源監控異常:
 i18n.select_workspace_to_modify.ac87=請選擇要修改的工作空間
 i18n.decode_failure.822e=解碼失敗
@@ -832,6 +860,8 @@ i18n.project_id_not_found.b87e=沒有專案id
 i18n.operation_type.de9c=操作型別
 i18n.project_has_logs_cannot_migrate.2e0e=當前專案存在日誌閱讀,不能直接遷移
 i18n.delete_failure_with_colon.b429=刪除失敗\:
+i18n.no_ftp_server_correspondence.1af7=沒有對應的Ftp
+i18n.ftp_import_template.8fa3=ftp匯入模板
 i18n.tag_cannot_contain_colon.f9ae=標籤不能包含 :
 i18n.distribute_id_already_exists_globally.6478=分發id已經存在啦,分發id需要全域性唯一
 i18n.import_exception.04b6=匯入第 {} 條資料異常\:{}
@@ -895,7 +925,9 @@ i18n.no_manager_node_found.5934=當前叢集未找到任何管理節點
 i18n.file_transfer_exception.bda6=轉發檔案異常
 i18n.select_node.f8a6=請選擇節點
 i18n.upload_progress_with_units.44ad=上傳檔案進度\:{} {}/{} {}
+i18n.ftp_download_file_failed.2e42=FTP 下載檔案失敗
 i18n.ssh_terminal_execution_log.58f1=ssh 終端執行日誌
+i18n.ftp_file_manager.c52e=FTP檔案管理
 i18n.project_monitor.d2ff=專案監控
 i18n.push_image_interrupted.6377=push image 被中斷\:
 i18n.default_value.1aa9={} [預設]
@@ -986,6 +1018,7 @@ i18n.certificate_already_exists.adf9=當前證書已經存在啦(系統全域性
 i18n.oshi_file_system_monitoring_exception.dc24=oshi 檔案系統資源監控異常
 i18n.image_cannot_be_empty.1600=映象不能為空
 i18n.no_corresponding_node_info.cd24=沒有對應到節點資訊
+i18n.auto_backup_path.a16b=自動備份資料檔案到路徑
 i18n.node_plugin_version_required.2318=node 外掛 version 不能為空
 i18n.start_new_thread_for_h2_database_backup.9337=啟動一個新執行緒來執行 H2 資料庫備份...啟動
 i18n.docker_exec_terminal_process_ended.c734=[{}] docker exec 終端程序結束
@@ -1005,6 +1038,7 @@ i18n.protocol_not_supported.b906=不支援的 protocol
 i18n.force_unbind_succeeded.5bfd=強制解綁成功
 i18n.unexpected_exception.2b52=發生異常
 i18n.check_email_error.636c=檢查郵箱資訊錯誤:{}
+i18n.ftp_unauthorized_directory.df73=此ftp未授權操作此目錄
 i18n.initialization_success.4725=初始化成功
 i18n.running_status.d679=執行中
 i18n.auto_clean_temp_dir.11d2=自動清理臨時目錄
@@ -1101,6 +1135,7 @@ i18n.batch_trigger_script_exception.8fb4=服務端指令碼批量觸發異常
 i18n.upgrade_failure.4ae2=升級失敗
 i18n.no_branch_or_tag_message.8ae3=沒有 {} 分支/標籤
 i18n.general_error_message.728a=啊哦,好像哪裡出錯了,請稍候再試試吧~
+i18n.ftp_connection_failure.0f31=關閉 FTP 連線失敗
 i18n.service_info_incomplete_with_code2.e9ca=服務資訊不完整不能操作:-2
 i18n.ignored_operation.edee=忽略的操作:{}
 i18n.monitor_name_cannot_be_empty.514a=監控名稱不能為空
@@ -1115,6 +1150,7 @@ i18n.select_related_data_id.7fba=請選擇關聯資料 ID
 i18n.node_name_required.5bdf=請填寫節點名稱
 i18n.client_terminated_connection.6886=客戶端終止連線:{}
 i18n.close_thread_pool.4cd9=關閉 {} 執行緒池
+i18n.ftp_upload_file_exception.118c=FTP上傳檔案異常
 i18n.clear_ip_whitelist_config_success.8cf6=清除 IP 白名單配置成功
 i18n.no_corresponding_folder.621f=沒有對應資料夾
 i18n.no_online_manager_node_found.05d7=當前叢集未找到線上的管理節點
@@ -1124,6 +1160,7 @@ i18n.execution_completed.24a1=執行完畢\:
 i18n.close_project_failure.a1d2=關閉專案失敗:
 i18n.select_folder_to_compress.915f=請選擇資料夾進行壓縮
 i18n.config_file_not_found.fc87=均未找到配置檔案
+i18n.ftp_item_not_found.60a7=沒有找到對應的ftp項:{}
 i18n.update_ssh_machine_id_failed.bd24=更新 SSH 表機器id 失敗:
 i18n.select_pull_code_protocol.fc24=請選擇拉取程式碼的協議
 i18n.auth_directory_cannot_be_empty.21ba=授權目錄不能為空
@@ -1291,6 +1328,7 @@ i18n.program_already_running.96e1=當前程式正在執行中,不能重複啟
 i18n.password_change_success.8013=修改密碼成功!
 i18n.build_resource_cleanup_failed.c4cf=清理構建資源失敗
 i18n.incorrect_publish_method.e095=釋出方法不正確
+i18n.ftp_directory.a790=請輸入釋出到ftp中的目錄
 i18n.import_low_version_data_to_new_version.247b=2. 將匯出的低版本資料( sql 檔案) 匯入到新版本中【啟動程式引數裡面新增 --replace-import-h2-sql\=/xxxx.sql (路徑需要替換為第一步控制檯輸出的 sql 檔案儲存路徑)】
 i18n.incorrect_line_number.5877=行號不正確
 i18n.file_deletion_event_with_details.7537=檔案刪除事件:{} {}
@@ -1299,6 +1337,7 @@ i18n.maven_plugin_version_required.71f1=maven 外掛 version 不能為空
 i18n.trigger_token_error_or_expired_with_code.393b=觸發token錯誤,或者已經失效\:-1
 i18n.invalid_webhooks_address.d836=WebHooks 地址不合法
 i18n.project_path_auth_not_under_jpom.0e18=專案路徑授權不能位於Jpom目錄下
+i18n.ftp_rename_failed_exception.0fcc=FTP重新命名失敗異常
 i18n.reconnect_failure.7c01=重連失敗
 i18n.project_operations.03d9=專案運維
 i18n.image_not_exist.ee17=映象不存在
@@ -1338,6 +1377,7 @@ i18n.refreshing_cache.c969=正在重新整理快取中,請勿重複重新整理
 i18n.start_executing_database_event.fc57=開始執行資料庫事件:{}
 i18n.static_file_storage.35f6=靜態檔案儲存
 i18n.project_log_is_existing_folder.a80a=專案log是一個已經存在的資料夾
+i18n.check_ftp_connection_failed.f7de=檢測 FTP 連線失效 [{}],準備重建\: {}
 i18n.project_data_workspace_id_inconsistency.7ed6=專案資料工作空間ID[{}]查詢出節點ID不一致, 舊資料\: {}, 新資料\: {}
 i18n.query_workspace_error.6a0d=查詢錯誤的工作空間失敗
 i18n.unknown_database_mode.f9e5=當前資料庫模式未知
@@ -1375,8 +1415,8 @@ i18n.token_invalid_or_expired.cb96=token錯誤,或者已經失效\:-1
 i18n.list_and_query.c783=列表、查詢
 i18n.migration_docker_cert_error.a5ea=遷移 docker[{}] 證書發生異常
 i18n.file_downloading.7a8f=檔案下載中
-i18n.mark_must_contain_letters_numbers_underscores.667d=標記只能包含字母、數字、下劃線
 i18n.please_fill_in_runs_on.c2ff=請填寫runsOn。
+i18n.mark_must_contain_letters_numbers_underscores.667d=標記只能包含字母、數字、下劃線
 i18n.create_build_task_exception.06f1=建立構建任務異常
 i18n.execution_interrupted.1bb6=執行被中斷
 i18n.event_loss_or_execution_error.7b14=事件丟失或者執行錯誤:{} {}
@@ -1401,6 +1441,7 @@ i18n.please_pass_parameter.3182=請傳入引數
 i18n.node_no_data_pulled.0dae={} 節點沒有拉取到任何 {},但是刪除了資料:{}
 i18n.node_info_not_found.2c8c=沒有查詢到節點資訊:
 i18n.please_fill_in_repository_address.0cf8=請填寫倉庫地址
+i18n.no_ftp_correspondence.23c4=沒有對應的FTP
 i18n.table_without_primary_key.7392=表沒有主鍵
 i18n.invalid_shard_id.46fd=不合法的分片id
 i18n.execution_ended.b793=執行結束\:{}
@@ -1499,6 +1540,7 @@ i18n.config_file_database_config_not_parsed.47b2=未解析出配置檔案中的
 i18n.data_already_exists.0397=匯入的資料已經存在啦
 i18n.please_check_in_time.3b4f=請及時檢查
 i18n.installation_success.811f=安裝成功
+i18n.timeout.e944=超時
 i18n.cluster_management.74ea=叢集管理
 i18n.type_field_value_error.14cf=第 {} 行 type 欄位值錯誤(Git/Svn)
 i18n.migrate_data.f556=遷移資料
@@ -1514,6 +1556,7 @@ i18n.backup_data_trigger.a71a=備份資料觸發器
 i18n.file_signature_info_not_found.83bf=沒有檔案簽名資訊
 i18n.associated_nodes_warning.64d8=當前機器還關聯{}個節點,不能直接刪除(需要提前解綁或者刪除關聯資料後才能刪除)
 i18n.build_unknown_error.dad6=構建發生未知錯誤
+i18n.start_import_data.ea31=開始匯入資料:{}
 i18n.new_version_exists_download_unavailable.4ba7=存在新版本,下載地址不可用
 i18n.compare_backup_failure.303e=對比清空專案檔案備份失敗
 i18n.upload_success_and_restart.7bc3=上傳成功並重啟
@@ -1547,9 +1590,11 @@ i18n.download_exception.e616=下載檔案異常
 i18n.process_file_deletion_exception.1c6e=處理檔案刪除異常
 i18n.trigger_auto_execute_command_template_exception.4e01=觸發自動執行命令模版異常
 i18n.no_build.d163=沒有對應的構建
+i18n.ftp_management.cb91=FTP管理
 i18n.start_executing_publishing_with_file_size.5039=開始執行釋出,需要釋出的檔案大小:{}
 i18n.command_execution_exception.4ccd=執行命令異常
 i18n.operation_ip.cbd4=操作IP
+i18n.exception.c195=異常
 i18n.auth_directory_cannot_contain_hierarchy.d6ca=授權目錄中不能存在包含關係:
 i18n.ip_authorization_interception_exception.8130=IP授權攔截異常,請檢查配置是否正確
 i18n.refresh_token_timeout.3291=重新整理token超時

+ 45 - 0
modules/common/src/main/resources/i18n/words.json

@@ -15,6 +15,7 @@
 	"i18n.address_not_configured.f2eb":"未配置地址",
 	"i18n.admin_account_required.31e0":"系统中的系统管理员账号数量必须存在一个以上",
 	"i18n.admin_email_not_configured.ecb8":"管理员还没有配置系统邮箱,请联系管理配置发件信息",
+	"i18n.affected_rows.5781":"影响行数",
 	"i18n.agent_jar_damaged.74a8":"Agent JAR 损坏请重新上传,",
 	"i18n.agent_jar_not_exist.28ac":"Agent JAR包不存在",
 	"i18n.agent_response_empty.cc8e":"agent 端响应内容为空",
@@ -27,6 +28,7 @@
 	"i18n.alias_or_token_error.d5c6":"别名或者token错误,或者已经失效",
 	"i18n.already_offline.d3b5":"已经离线啦",
 	"i18n.asset_cluster_and_node_mismatch.8964":"资产集群和节点不匹配",
+	"i18n.asset_ftp_info.3b75":"资产FTP信息",
 	"i18n.asset_machine_node_statistics.4a03":"资产机器节点统计",
 	"i18n.asset_monitoring_thread_pool_rejected_task.222e":"资产监控线程池拒绝了任务:{}",
 	"i18n.asset_ssh_not_exist.cd43":"不存在对应的资产SSH",
@@ -54,6 +56,7 @@
 	"i18n.authorization_exception.acc0":"{} 授权异常 {}",
 	"i18n.authorized_cannot_be_reloaded.6ece":"authorized 不能重复加载",
 	"i18n.auto_backup_h2_database.2ed0":"自动备份 h2 数据库文件,备份文件位于:{}",
+	"i18n.auto_backup_path.a16b":"自动备份数据文件到路径",
 	"i18n.auto_clean_temp_dir.11d2":"自动清理临时目录",
 	"i18n.auto_clear_data_errors.112f":"自动清除数据错误 {} {}",
 	"i18n.auto_clear_machine_node_stats_logs.5279":"自动清理 {} 条机器节点统计日志",
@@ -157,6 +160,7 @@
 	"i18n.check_docker_exception.a6d1":"检查 docker 异常",
 	"i18n.check_docker_url_exception.4302":"检查 docker url 异常 {}",
 	"i18n.check_email_error.636c":"检查邮箱信息错误:{}",
+	"i18n.check_ftp_connection_failed.f7de":"检测 FTP 连接失效 [{}],准备重建: {}",
 	"i18n.check_git_client_exception.42a3":"检查 git 客户端异常",
 	"i18n.check_passed.dce8":"检查通过",
 	"i18n.checkout_version.a586":"把版本:%s check out ",
@@ -209,6 +213,7 @@
 	"i18n.cluster_not_bound_to_group_for_node_monitoring.1586":"当前集群还未绑定分组,不能监控集群节点资产信息",
 	"i18n.cluster_not_bound_to_group_for_ssh_monitoring.c894":"当前集群还未绑定分组,不能监控 SSH 资产信息",
 	"i18n.cluster_not_exist.4098":"对应的集群不存在",
+	"i18n.cluster_not_grouped.8f54":"当前集群还未绑定分组,不能监控 FTP 资产信息",
 	"i18n.cluster_response_incorrect.c08a":"集群响应信息不正确,请确认集群地址是正确的服务端地址",
 	"i18n.cluster_status_code_exception.9d89":"集群状态码异常:{} {}",
 	"i18n.code_pull_conflict.6e8e":"拉取代码发生冲突,可以尝试清除构建或者解决仓库里面的冲突后重新操作。:",
@@ -346,12 +351,14 @@
 	"i18n.data_type_not_supported.fd03":"不支持的数据类型:",
 	"i18n.data_workspace_mismatch.ae1d":"数据工作空间和操作工作空间不一致",
 	"i18n.database_auto_backup_support.7b8f":"当前数据库不支持自动备份",
+	"i18n.database_backup_complete_path.861b":"数据库备份完成,保存路径为",
 	"i18n.database_backup_label.62d8":"数据库备份",
 	"i18n.database_connection_not_configured.c80e":"没有配置数据库连接",
 	"i18n.database_corrupted.944e":"数据库异常,可能数据库文件已经损坏(可能丢失部分数据),需要重新初始化。可以尝试在启动参数里面添加 --recover:h2db 来自动恢复,:",
 	"i18n.database_event_execution_ended.690b":"数据库 {} 事件执行结束,:{}",
 	"i18n.database_exception.4894":"数据库异常",
 	"i18n.database_exception_due_to_resources.dbf1":"数据库异常,可能因为服务器资源不足(内存、硬盘)等原因造成数据异常关闭。需要手动重启服务端来恢复,:",
+	"i18n.database_load_success_url.5f64":"数据库加载成功,URL为",
 	"i18n.database_mode_config_missing.ae5d":"数据库Mode配置缺失",
 	"i18n.database_not_initialized.e5e7":"还没有初始化数据库",
 	"i18n.database_username_not_configured.a048":"未配置(未解析到)数据库用户名",
@@ -487,6 +494,7 @@
 	"i18n.event_script_does_not_exist.e726":"事件脚本不存在:{} {}",
 	"i18n.event_script_interrupted.8c79":"事件脚本中断:",
 	"i18n.event_type_not_supported.e9c3":"不支持的事件类型:{}",
+	"i18n.exception.c195":"异常",
 	"i18n.exclusion_success.7d46":"剔除成功",
 	"i18n.execute.1a6a":"执行",
 	"i18n.execute_dsl_script_exception.0882":"执行 DSL 脚本异常:{}",
@@ -514,6 +522,7 @@
 	"i18n.exit_successful.8150":"退出成功",
 	"i18n.export_image_exception.cb1c":"导出镜像异常",
 	"i18n.export_low_version_data.f1aa":"1. 导出低版本数据 【启动程序参数里面添加 --backup-h2】",
+	"i18n.exported_ftp_data.2b54":"导出的 ftp 数据",
 	"i18n.exported_project_data.fd1f":"导出的项目数据 ",
 	"i18n.exported_repo_data.bac5":"导出的 仓库信息 数据 ",
 	"i18n.exported_ssh_data.ce88":"导出的 ssh 数据",
@@ -532,6 +541,7 @@
 	"i18n.file_download_failed.7983":"文件下载失败:",
 	"i18n.file_downloading.7a8f":"文件下载中",
 	"i18n.file_downloading_status.c995":"文件下载中:",
+	"i18n.file_exists.145b":"文件夹或文件已经存在",
 	"i18n.file_format_not_supported.eac4":"不支持的文件格式",
 	"i18n.file_full_path.16cc":"文件全路径:{}",
 	"i18n.file_id_missing.0e39":"文件 ID 缺失",
@@ -547,6 +557,8 @@
 	"i18n.file_name_not_found.b0ed":"没有文件名",
 	"i18n.file_not_exist.5091":"对应的文件不存在",
 	"i18n.file_not_exist.ea6a":"不存在对应的文件",
+	"i18n.file_not_exist_or_unable_to_download.b977":"文件不存在或无法下载:",
+	"i18n.file_not_exist_or_unable_to_open.b045":"文件不存在或无法打开:",
 	"i18n.file_not_found.d952":"文件不存在",
 	"i18n.file_or_directory_not_found.f03e":"文件不存在或者是目录:",
 	"i18n.file_publish_task_record.edc4":"文件发布任务记录",
@@ -581,6 +593,29 @@
 	"i18n.forbidden_operation_time_range.92bf":"【禁止操作】当前时段禁止执行 {} 至 {}",
 	"i18n.force_unbind_succeeded.5bfd":"强制解绑成功",
 	"i18n.free_script.7760":"自由脚本",
+	"i18n.ftp_already_exists.d66b":"对应的FTP已经存在啦",
+	"i18n.ftp_asset_management.c6a5":"FTP资产管理",
+	"i18n.ftp_client_build_failure.aa55":"构建 FTP 客户端失败 [{}]: {}",
+	"i18n.ftp_connection_failed.1f2f":"连接FTP失败",
+	"i18n.ftp_connection_failed_message.bd99":"连接FTP失败:",
+	"i18n.ftp_connection_failure.0f31":"关闭 FTP 连接失败",
+	"i18n.ftp_connection_or_operation_exception.09af":"FTP 连接或操作异常",
+	"i18n.ftp_create_folder_exception.a4fe":"FTP创建文件夹异常",
+	"i18n.ftp_directory.a790":"请输入发布到ftp中的目录",
+	"i18n.ftp_download_file_failed.2e42":"FTP 下载文件失败",
+	"i18n.ftp_file_manager.c52e":"FTP文件管理",
+	"i18n.ftp_folder_query_failed.0011":"无法查询文件夹FTP,",
+	"i18n.ftp_import_template.8fa3":"ftp导入模板",
+	"i18n.ftp_info_table.b177":"ftp信息表",
+	"i18n.ftp_item_not_found.60a7":"没有找到对应的ftp项:{}",
+	"i18n.ftp_management.cb91":"FTP管理",
+	"i18n.ftp_not_exist.f9b3":"不存在对应ftp",
+	"i18n.ftp_read_file_failed.e738":"FTP 读取文件失败",
+	"i18n.ftp_rename_failed_exception.0fcc":"FTP重命名失败异常",
+	"i18n.ftp_selection.c903":"请选择分发FTP项",
+	"i18n.ftp_unauthorized_directory.df73":"此ftp未授权操作此目录",
+	"i18n.ftp_upload_failed.8298":"FTP 上传失败",
+	"i18n.ftp_upload_file_exception.118c":"FTP上传文件异常",
 	"i18n.fuzzy_match_files.139d":"{} 模糊匹配到 {} 个文件",
 	"i18n.general_error_message.728a":"啊哦,好像哪里出错了,请稍候再试试吧~",
 	"i18n.general_execution_exception.62e9":"执行异常:",
@@ -647,6 +682,7 @@
 	"i18n.import_save_failure.001a":"导入第 {} 条数据保存失败:{}",
 	"i18n.import_save_project_exception.cdbe":"导入保存项目异常",
 	"i18n.import_success.b6d1":"导入成功",
+	"i18n.import_success.ef46":"导入成功:{}",
 	"i18n.import_success_message.2df3":"导入成功(编码格式:{}),更新 {} 条数据,因为节点分发/项目副本忽略 {} 条数据",
 	"i18n.import_success_with_count.22b9":"导入成功,添加 {} 条数据,修改 {} 条数据",
 	"i18n.import_success_with_details.a4a0":"导入成功(编码格式:{}),添加 {} 条数据,修改 {} 条数据",
@@ -677,6 +713,7 @@
 	"i18n.initialization_failure.19e9":"初始化失败:",
 	"i18n.initialization_success.4725":"初始化成功",
 	"i18n.initialize_database_failure.2ef9":"初始化数据库失败 {}",
+	"i18n.initialize_sql.6691":"执行初始化SQL文件",
 	"i18n.initialize_user_failure.fe27":"初始化用户失败",
 	"i18n.initialize_workspace.bc97":"初始化{}工作空间",
 	"i18n.install_id_does_not_exist.6aee":"数据错误,安装 ID 不存在",
@@ -803,6 +840,7 @@
 	"i18n.monitor_docker_exception_detail.e334":"监控 docker[{}] 异常 {}",
 	"i18n.monitor_docker_timeout.b03b":"监控 docker[{}] 超时 {}",
 	"i18n.monitor_info.f299":"监控信息",
+	"i18n.monitor_name.9aff":"监控",
 	"i18n.monitor_name_cannot_be_empty.514a":"监控名称不能为空",
 	"i18n.monitor_node_exception.6ff1":"监控 {} 节点异常 {}",
 	"i18n.monitor_ssh_exception.e9ce":"监控 ssh[{}] 异常",
@@ -904,6 +942,9 @@
 	"i18n.no_file_info.db01":"没有对应的文件信息",
 	"i18n.no_files_in_project_directory.108e":"项目目录没有任何文件,请先到项目文件管理中上传文件",
 	"i18n.no_files_in_zip.1af6":"压缩包里没有任何文件",
+	"i18n.no_ftp_correspondence.23c4":"没有对应的FTP",
+	"i18n.no_ftp_item.8e39":"没有对应的ftp项",
+	"i18n.no_ftp_server_correspondence.1af7":"没有对应的Ftp",
 	"i18n.no_get_id_method.2a65":"没有  getId 方法",
 	"i18n.no_h2_data_info_for_migration.5799":"没有 h2 数据信息不用迁移",
 	"i18n.no_implemented_feature.af80":"没有实现该功能",
@@ -919,6 +960,7 @@
 	"i18n.no_management_permission.fd25":"您没有对应管理权限:-2",
 	"i18n.no_management_permission2.35d4":"您没有对应管理权限:-3",
 	"i18n.no_manager_node_found.5934":"当前集群未找到任何管理节点",
+	"i18n.no_matching_asset_ftp.d420":"不存在对应的资产FTP",
 	"i18n.no_matching_data_found.fe9d":"未找到匹配的数据",
 	"i18n.no_matching_files.b7a6":"{} 没有匹配到任何文件",
 	"i18n.no_matching_permission.09cf":"未匹配到合适的权限不足",
@@ -1452,6 +1494,7 @@
 	"i18n.ssh_with_build_items_message.0f6d":"当前ssh存在构建项,不能直接删除(需要提前解绑或者删除关联数据后才能删除)",
 	"i18n.ssl_connection_failed.e26c":"SSL 无法连接(请检查证书信任的地址和配置的 docker host 是否一致):",
 	"i18n.start_async_download.78cc":"开始异步下载",
+	"i18n.start_backup_database.e554":"开始备份数据库",
 	"i18n.start_building.1039":"开始构建中",
 	"i18n.start_building_image.eacd":"{} 开始构建镜像 {}{}",
 	"i18n.start_building_with_number_and_path.c41c":"开始构建 #{} 构建执行路径 : {}",
@@ -1474,6 +1517,7 @@
 	"i18n.start_executing_upload_post_command.1c1b":"开始执行上传后命令",
 	"i18n.start_executing_upload_pre_command.fb5c":"开始执行上传前命令",
 	"i18n.start_execution.00d7":"开始执行",
+	"i18n.start_import_data.ea31":"开始导入数据:{}",
 	"i18n.start_loading_database.b040":"开始加载 {} 数据库",
 	"i18n.start_migrating.20d6":"开始迁移 {} {}",
 	"i18n.start_migrating_h2_data_to.f478":"开始迁移 h2 数据到 {}",
@@ -1549,6 +1593,7 @@
 	"i18n.temp_folder_file_count.8e31":"临时文件夹累计文件数:{},处理成功数:{}",
 	"i18n.temporary_result_file_does_not_exist.1c7e":"临时结果文件不存在: {}",
 	"i18n.test_result.8441":"测试结果:{} {}",
+	"i18n.timeout.e944":"超时",
 	"i18n.token_invalid_or_expired.cb96":"token错误,或者已经失效:-1",
 	"i18n.token_parse_failed.cadf":"token 解析失败:",
 	"i18n.too_many_attempts.d88d":"尝试次数太多,请稍后再来",

+ 4 - 5
modules/common/src/test/resources/baidubce_translate.txt

@@ -1,19 +1,18 @@
 You are the service that converts a user request JSON into a new (user-expected) JSON object based on the following JavaScript-defined JSON object:
 
-```
 // 将下面json中的 根据`值`的含义将 `key` 转为语义化且简短的首字母为小写的小驼峰英文变量名替换无意义字符串
 // 此处进行替换,禁止出现 k1 k2 k3
-const template = {
+```json
+{
  "key1": "string1",
  "key2": "string2",
 }
 ```
 
 The following is a user request:
-```
-const template = {REQUEST_STR}
+```json
+{REQUEST_STR}
 ```
 
 // 要求输出的请求和结果分开,不要输出到一个代码片段并且是 json 代码片段
-// 输出的变量请使用 var 语法
 // 输出的 json 结果中不要声明注释

+ 1 - 1
modules/server/DockerfileBeta

@@ -16,7 +16,7 @@ LABEL maintainer="bwcx-jzy <bwcx_jzy@163.com>"
 LABEL documentation="https://jpom.top"
 
 ENV JPOM_HOME	/usr/local/jpom-server
-ENV JPOM_PKG_VERSION	2.11.11.4
+ENV JPOM_PKG_VERSION	2.11.12.1
 ENV JPOM_PKG	server-${JPOM_PKG_VERSION}-release.tar.gz
 ENV SHA1_NAME server-${JPOM_PKG_VERSION}-release.tar.gz.sha1
 

+ 1 - 1
modules/server/DockerfileBetaJdk17

@@ -16,7 +16,7 @@ LABEL maintainer="bwcx-jzy <bwcx_jzy@163.com>"
 LABEL documentation="https://jpom.top"
 
 ENV JPOM_HOME	/usr/local/jpom-server
-ENV JPOM_PKG_VERSION	2.11.11.4
+ENV JPOM_PKG_VERSION	2.11.12.1
 ENV JPOM_PKG	server-${JPOM_PKG_VERSION}-release.tar.gz
 ENV SHA1_NAME server-${JPOM_PKG_VERSION}-release.tar.gz.sha1
 

+ 14 - 2
modules/server/pom.xml

@@ -16,13 +16,13 @@
     <parent>
         <artifactId>jpom-parent</artifactId>
         <groupId>org.dromara.jpom</groupId>
-        <version>2.11.12</version>
+        <version>2.11.12.1</version>
         <relativePath>../../pom.xml</relativePath>
     </parent>
     <modelVersion>4.0.0</modelVersion>
     <name>Jpom Server</name>
     <artifactId>server</artifactId>
-    <version>2.11.12</version>
+    <version>2.11.12.1</version>
     <properties>
         <start-class>org.dromara.jpom.JpomServerApplication</start-class>
     </properties>
@@ -140,6 +140,12 @@
             <version>${project.version}</version>
         </dependency>
 
+        <dependency>
+            <groupId>org.dromara.jpom.storage-module</groupId>
+            <artifactId>storage-module-dameng</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
         <dependency>
             <groupId>me.zhyd.oauth</groupId>
             <artifactId>JustAuth</artifactId>
@@ -162,6 +168,12 @@
             <groupId>cn.hutool</groupId>
             <artifactId>hutool-cache</artifactId>
         </dependency>
+
+        <dependency>
+            <groupId>commons-net</groupId>
+            <artifactId>commons-net</artifactId>
+            <version>3.11.1</version>
+        </dependency>
     </dependencies>
     <build>
         <resources>

+ 2 - 0
modules/server/src/main/java/org/dromara/jpom/JpomServerApplication.java

@@ -61,6 +61,8 @@ public class JpomServerApplication {
      * --h2-migrate-postgresql --h2-user=jpom --h2-pass=jpom 将 h2 数据库迁移到 postgresql
      * <p>
      * --h2-migrate-mariadb --h2-user=jpom --h2-pass=jpom 将 h2 数据库迁移到 mariadb
+     * <p>
+     * --h2-migrate-dameng --h2-user=jpom --h2-pass=jpom 将 h2 数据库迁移到 dameng
      *
      * @param args 参数
      * @throws Exception 异常

+ 8 - 0
modules/server/src/main/java/org/dromara/jpom/build/BuildExtraModule.java

@@ -73,6 +73,10 @@ public class BuildExtraModule extends BaseModel {
      * 发布到ssh中的目录
      */
     private String releasePath;
+    /**
+     * 发布到ftp中的目录
+     */
+    private String releaseFtpPath;
     /**
      * 工作空间 ID
      */
@@ -149,6 +153,10 @@ public class BuildExtraModule extends BaseModel {
      * 镜像标签
      */
     private String dockerImagesLabels;
+    /**
+     * 镜像网络模式
+     */
+    private String dockerImagesNetworkMode;
     /**
      * 项目二级目录
      */

+ 82 - 11
modules/server/src/main/java/org/dromara/jpom/build/ReleaseManage.java

@@ -7,6 +7,7 @@
  * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
  * See the Mulan PSL v2 for more details.
  */
+
 package org.dromara.jpom.build;
 
 import cn.hutool.core.collection.CollUtil;
@@ -22,6 +23,7 @@ import cn.hutool.core.util.NumberUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.crypto.SecureUtil;
+import cn.hutool.extra.ftp.Ftp;
 import cn.hutool.extra.spring.SpringUtil;
 import cn.hutool.extra.ssh.JschUtil;
 import cn.keepbx.jpom.model.JsonMessage;
@@ -30,6 +32,21 @@ import com.alibaba.fastjson2.JSONArray;
 import com.alibaba.fastjson2.JSONObject;
 import com.jcraft.jsch.ChannelSftp;
 import com.jcraft.jsch.Session;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
 import lombok.Builder;
 import lombok.Lombok;
 import lombok.extern.slf4j.Slf4j;
@@ -39,14 +56,17 @@ import org.dromara.jpom.common.forward.NodeForward;
 import org.dromara.jpom.common.forward.NodeUrl;
 import org.dromara.jpom.common.i18n.I18nMessageUtil;
 import org.dromara.jpom.configuration.BuildExtConfig;
+import org.dromara.jpom.func.assets.model.MachineFtpModel;
 import org.dromara.jpom.func.assets.model.MachineSshModel;
 import org.dromara.jpom.func.assets.server.MachineDockerServer;
+import org.dromara.jpom.func.assets.server.MachineFtpServer;
 import org.dromara.jpom.func.assets.server.ScriptLibraryServer;
 import org.dromara.jpom.func.files.service.FileStorageService;
 import org.dromara.jpom.model.AfterOpt;
 import org.dromara.jpom.model.BaseEnum;
 import org.dromara.jpom.model.EnvironmentMapBuilder;
 import org.dromara.jpom.model.data.BuildInfoModel;
+import org.dromara.jpom.model.data.FtpModel;
 import org.dromara.jpom.model.data.NodeModel;
 import org.dromara.jpom.model.data.SshModel;
 import org.dromara.jpom.model.docker.DockerInfoModel;
@@ -60,6 +80,7 @@ import org.dromara.jpom.plugins.JschUtils;
 import org.dromara.jpom.service.docker.DockerInfoService;
 import org.dromara.jpom.service.docker.DockerSwarmInfoService;
 import org.dromara.jpom.service.node.NodeService;
+import org.dromara.jpom.service.node.ftp.FtpService;
 import org.dromara.jpom.service.node.ssh.SshService;
 import org.dromara.jpom.system.ExtConfigBean;
 import org.dromara.jpom.system.JpomRuntimeException;
@@ -69,17 +90,6 @@ import org.dromara.jpom.util.MySftp;
 import org.dromara.jpom.util.StringUtil;
 import org.springframework.util.Assert;
 
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.charset.Charset;
-import java.util.*;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-
 /**
  * 发布管理
  *
@@ -110,6 +120,8 @@ public class ReleaseManage {
     private static BuildExtConfig buildExtConfig;
     private static FileStorageService fileStorageService;
     private static ScriptLibraryServer scriptLibraryServer;
+    private static MachineFtpServer machineFtpServer;
+
 
     private void loadService() {
         buildExecuteService = ObjectUtil.defaultIfNull(buildExecuteService, () -> SpringUtil.getBean(BuildExecuteService.class));
@@ -118,6 +130,7 @@ public class ReleaseManage {
         buildExtConfig = ObjectUtil.defaultIfNull(buildExtConfig, () -> SpringUtil.getBean(BuildExtConfig.class));
         fileStorageService = ObjectUtil.defaultIfNull(fileStorageService, () -> SpringUtil.getBean(FileStorageService.class));
         scriptLibraryServer = ObjectUtil.defaultIfNull(scriptLibraryServer, () -> SpringUtil.getBean(ScriptLibraryServer.class));
+        machineFtpServer = ObjectUtil.defaultIfNull(machineFtpServer, () -> SpringUtil.getBean(MachineFtpServer.class));
     }
 
     private Integer getRealBuildNumberId() {
@@ -198,6 +211,8 @@ public class ReleaseManage {
             return this.localCommand();
         } else if (releaseMethod == BuildReleaseMethod.DockerImage.getCode()) {
             return this.doDockerImage();
+        } else if (releaseMethod == BuildReleaseMethod.Ftp.getCode()) {
+            this.doFtp();
         } else if (releaseMethod == BuildReleaseMethod.No.getCode()) {
             return null;
         } else {
@@ -377,6 +392,7 @@ public class ReleaseManage {
         map.put("pull", extraModule.getDockerBuildPull());
         map.put("noCache", extraModule.getDockerNoCache());
         map.put("labels", extraModule.getDockerImagesLabels());
+        map.put("networkMode", extraModule.getDockerImagesNetworkMode());
         map.put("env", envMap);
         Consumer<String> logConsumer = logRecorder::append;
         map.put("logConsumer", logConsumer);
@@ -503,6 +519,61 @@ public class ReleaseManage {
         }
     }
 
+
+    /**
+     * ftp发布
+     *
+     * @throws IOException
+     */
+    private void doFtp() {
+        String releaseMethodDataId = this.buildExtraModule.getReleaseMethodDataId();
+        FtpService ftpService = SpringUtil.getBean(FtpService.class);
+        List<String> strings = StrUtil.splitTrim(releaseMethodDataId, StrUtil.COMMA);
+        for (String releaseMethodDataIdItem : strings) {
+            FtpModel item = ftpService.getByKey(releaseMethodDataIdItem, false);
+            if (item == null) {
+                logRecorder.systemError(I18nMessageUtil.get("i18n.ftp_item_not_found.60a7"), releaseMethodDataIdItem);
+                continue;
+            }
+
+            String releasePath = this.buildExtraModule.getReleaseFtpPath();
+
+            if (StrUtil.isEmpty(releasePath)) {
+                logRecorder.systemWarning(I18nMessageUtil.get("i18n.publish_directory_is_empty.79c6"));
+            } else {
+                logRecorder.system(I18nMessageUtil.get("i18n.start_upload_ftp_file.20be"), DateUtil.now(), item.getName(), System.lineSeparator());
+
+                MachineFtpModel machineFtpModel = ftpService.getMachineFtpModel(item);
+                try (Ftp ftp = machineFtpServer.getFtpClient(machineFtpModel)) {
+
+
+                    String prefix = "";
+                    if (!StrUtil.startWith(releasePath, StrUtil.SLASH)) {
+                        prefix = ftp.pwd();
+                    }
+                    String normalizePath = FileUtil.normalize(prefix + StrUtil.SLASH + releasePath);
+                    if (this.buildExtraModule.isClearOld()) {
+                        try {
+                            if (ftp.exist(normalizePath)) {
+                                ftp.delDir(normalizePath);
+                            }
+                        } catch (Exception e) {
+                            if (!StrUtil.startWithIgnoreCase(e.getMessage(), "No such file")) {
+                                logRecorder.error(I18nMessageUtil.get("i18n.clear_build_product_failed.edd4"), e);
+                            }
+                        }
+                    }
+                    ftpService.uploadWithProgress(ftp, this.resultFile, normalizePath, logRecorder, 5);
+                } catch (Exception e) {
+                    throw new RuntimeException(e);
+                }
+                logRecorder.system(I18nMessageUtil.get("i18n.start_upload_ftp_file.20be"), DateUtil.now(), item.getName(), System.lineSeparator());
+
+                logRecorder.system("{} {} ftp upload done", DateUtil.now(), item.getName());
+            }
+        }
+    }
+
     /**
      * 差异上传发布
      *

+ 29 - 0
modules/server/src/main/java/org/dromara/jpom/configuration/AssetsConfig.java

@@ -41,6 +41,10 @@ public class AssetsConfig {
      * ssh 资产配置
      */
     private SshConfig ssh;
+    /**
+     * ftp 资产配置
+     */
+    private FtpConfig ftp;
     /**
      * docker 资产配置
      */
@@ -53,6 +57,13 @@ public class AssetsConfig {
         });
     }
 
+    public FtpConfig getFtp() {
+        return ObjectUtil.defaultIfNull(this.ftp, () -> {
+            this.ftp = new FtpConfig();
+            return ftp;
+        });
+    }
+
     public DockerConfig getDocker() {
         return ObjectUtil.defaultIfNull(this.docker, () -> {
             this.docker = new DockerConfig();
@@ -78,6 +89,24 @@ public class AssetsConfig {
 
     }
 
+    /**
+     * ftp 配置
+     */
+    @Data
+    @ConfigurationProperties("jpom.assets.ftp")
+    public static class FtpConfig {
+
+        /**
+         * 监控频率
+         */
+        private String monitorCron;
+        /**
+         * 禁用监控的分组名 (如果想禁用所有配置 * 即可)
+         */
+        private List<String> disableMonitorGroupName;
+
+    }
+
     /**
      * docker 配置
      */

+ 5 - 1
modules/server/src/main/java/org/dromara/jpom/controller/DataStatController.java

@@ -10,10 +10,12 @@
 package org.dromara.jpom.controller;
 
 import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
 import cn.hutool.db.Entity;
 import cn.keepbx.jpom.IJsonMessage;
 import cn.keepbx.jpom.model.JsonMessage;
 import lombok.extern.slf4j.Slf4j;
+import org.dromara.jpom.dialect.DialectUtil;
 import org.dromara.jpom.func.assets.server.MachineDockerServer;
 import org.dromara.jpom.func.assets.server.MachineNodeServer;
 import org.dromara.jpom.func.assets.server.MachineSshServer;
@@ -145,7 +147,9 @@ public class DataStatController {
                 long disableUserCount = userService.count(userModel);
                 map.put("disableUserCount", disableUserCount);
             }
-            String sql = "select count(1) from " + userService.getTableName() + " where twoFactorAuthKey is null or twoFactorAuthKey=''";
+            String twoFactorAuthKey = DialectUtil.wrapField("twoFactorAuthKey");
+            String sql = StrUtil.format("select count(1) from {} where {} is null or {}=''",
+                userService.getTableName(),twoFactorAuthKey,twoFactorAuthKey);
             Number closeTwoFactorAuth = ObjectUtil.defaultIfNull(userService.queryNumber(sql), 0);
             map.put("openTwoFactorAuth", count - closeTwoFactorAuth.intValue());
         }

+ 44 - 1
modules/server/src/main/java/org/dromara/jpom/controller/build/BuildInfoController.java

@@ -39,6 +39,7 @@ import org.dromara.jpom.model.AfterOpt;
 import org.dromara.jpom.model.BaseEnum;
 import org.dromara.jpom.model.PageResultDto;
 import org.dromara.jpom.model.data.BuildInfoModel;
+import org.dromara.jpom.model.data.FtpModel;
 import org.dromara.jpom.model.data.RepositoryModel;
 import org.dromara.jpom.model.data.SshModel;
 import org.dromara.jpom.model.enums.BuildReleaseMethod;
@@ -51,6 +52,7 @@ import org.dromara.jpom.service.dblog.BuildInfoService;
 import org.dromara.jpom.service.dblog.DbBuildHistoryLogService;
 import org.dromara.jpom.service.dblog.RepositoryService;
 import org.dromara.jpom.service.docker.DockerInfoService;
+import org.dromara.jpom.service.node.ftp.FtpService;
 import org.dromara.jpom.service.node.ssh.SshService;
 import org.dromara.jpom.service.script.ScriptServer;
 import org.dromara.jpom.util.CommandUtil;
@@ -78,6 +80,7 @@ public class BuildInfoController extends BaseServerController {
 
     private final DbBuildHistoryLogService dbBuildHistoryLogService;
     private final SshService sshService;
+    private final FtpService ftpService;
     private final BuildInfoService buildInfoService;
     private final RepositoryService repositoryService;
     private final BuildExecuteService buildExecuteService;
@@ -87,7 +90,7 @@ public class BuildInfoController extends BaseServerController {
     protected final MachineDockerServer machineDockerServer;
 
     public BuildInfoController(DbBuildHistoryLogService dbBuildHistoryLogService,
-                               SshService sshService,
+                               SshService sshService, FtpService ftpService,
                                BuildInfoService buildInfoService,
                                RepositoryService repositoryService,
                                BuildExecuteService buildExecuteService,
@@ -97,6 +100,7 @@ public class BuildInfoController extends BaseServerController {
                                MachineDockerServer machineDockerServer) {
         this.dbBuildHistoryLogService = dbBuildHistoryLogService;
         this.sshService = sshService;
+        this.ftpService = ftpService;
         this.buildInfoService = buildInfoService;
         this.repositoryService = repositoryService;
         this.buildExecuteService = buildExecuteService;
@@ -262,6 +266,8 @@ public class BuildInfoController extends BaseServerController {
             // dockerSwarmId default
             String dockerSwarmId = this.formatDocker(jsonObject, request);
             jsonObject.put("releaseMethodDataId", dockerSwarmId);
+        } else if (releaseMethod1 == BuildReleaseMethod.Ftp){
+            this.formatFtp(jsonObject, request);
         }
         // 检查关联数据ID
         buildInfoModel.setReleaseMethodDataId(jsonObject.getString("releaseMethodDataId"));
@@ -355,6 +361,43 @@ public class BuildInfoController extends BaseServerController {
         jsonObject.put("releaseMethodDataId", releaseMethodDataId);
     }
 
+    /**
+     * 验证构建信息
+     * 当发布方式为【FTP】的时候
+     *
+     * @param jsonObject 配置信息
+     */
+    private void formatFtp(JSONObject jsonObject, HttpServletRequest request) {
+        // 发布方式
+        String releaseMethodDataId = jsonObject.getString("releaseMethodDataId_6");
+        Assert.hasText(releaseMethodDataId, I18nMessageUtil.get("i18n.ftp_selection.c903"));
+
+        String releasePath = jsonObject.getString("releasePath");
+        Assert.hasText(releasePath, I18nMessageUtil.get("i18n.ftp_directory.a790"));
+        releasePath = FileUtil.normalize(releasePath);
+        List<String> strings = StrUtil.splitTrim(releaseMethodDataId, StrUtil.COMMA);
+        for (String releaseMethodDataIdItem : strings) {
+            FtpModel ftpServiceItem = ftpService.getByKey(releaseMethodDataIdItem, request);
+            Assert.notNull(ftpServiceItem, I18nMessageUtil.get("i18n.no_ftp_item.8e39"));
+            //
+            if (releasePath.startsWith(StrUtil.SLASH)) {
+                // 以根路径开始
+                List<String> fileDirs = ftpServiceItem.fileDirs();
+                Assert.notEmpty(fileDirs, ftpServiceItem.getName() + I18nMessageUtil.get("i18n.ftp_unauthorized_directory.df73"));
+
+                boolean find = false;
+                for (String fileDir : fileDirs) {
+                    if (FileUtil.isSub(new File(fileDir), new File(releasePath))) {
+                        find = true;
+                    }
+                }
+                Assert.state(find, ftpServiceItem.getName() + I18nMessageUtil.get("i18n.ftp_unauthorized_directory.df73"));
+            }
+
+        }
+        jsonObject.put("releaseMethodDataId", releaseMethodDataId);
+    }
+
     private String formatDocker(JSONObject jsonObject, HttpServletRequest request) {
         // 发布命令
         String dockerfile = jsonObject.getString("dockerfile");

+ 211 - 0
modules/server/src/main/java/org/dromara/jpom/controller/ftp/FtpController.java

@@ -0,0 +1,211 @@
+/*
+ * Copyright (c) 2019 Of Him Code Technology Studio
+ * Jpom is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ * 			http://license.coscl.org.cn/MulanPSL2
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+package org.dromara.jpom.controller.ftp;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.tree.Tree;
+import cn.hutool.core.lang.tree.TreeNode;
+import cn.hutool.core.lang.tree.TreeUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.SecureUtil;
+import cn.keepbx.jpom.IJsonMessage;
+import cn.keepbx.jpom.model.JsonMessage;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import javax.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.jpom.common.BaseServerController;
+import org.dromara.jpom.common.i18n.I18nMessageUtil;
+import org.dromara.jpom.common.validator.ValidatorItem;
+import org.dromara.jpom.common.validator.ValidatorRule;
+import org.dromara.jpom.func.assets.server.MachineFtpServer;
+import org.dromara.jpom.model.PageResultDto;
+import org.dromara.jpom.model.data.FtpModel;
+import org.dromara.jpom.model.data.NodeModel;
+import org.dromara.jpom.model.enums.BuildReleaseMethod;
+import org.dromara.jpom.permission.ClassFeature;
+import org.dromara.jpom.permission.Feature;
+import org.dromara.jpom.permission.MethodFeature;
+import org.dromara.jpom.permission.SystemPermission;
+import org.dromara.jpom.service.dblog.BuildInfoService;
+import org.dromara.jpom.service.node.ftp.FtpService;
+import org.springframework.http.MediaType;
+import org.springframework.util.Assert;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author wxy
+ * @since 2025/05/29
+ */
+@RestController
+@RequestMapping(value = "node/ftp")
+@Feature(cls = ClassFeature.FTP)
+@Slf4j
+public class FtpController extends BaseServerController {
+
+    private final FtpService ftpService;
+    private final MachineFtpServer machineFtpServer;
+
+
+    private final BuildInfoService buildInfoService;
+
+    public FtpController(FtpService ftpService, MachineFtpServer machineFtpServer,
+                         BuildInfoService buildInfoService) {
+        this.ftpService = ftpService;
+        this.machineFtpServer = machineFtpServer;
+        this.buildInfoService = buildInfoService;
+    }
+
+
+    @PostMapping(value = "list_data.json", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.LIST)
+    public IJsonMessage<PageResultDto<FtpModel>> listData(HttpServletRequest request) {
+        PageResultDto<FtpModel> pageResultDto = ftpService.listPage(request);
+        pageResultDto.each(ftpModel -> {
+            ftpModel.setMachineFtp(machineFtpServer.getByKey(ftpModel.getMachineFtpId()));
+           /* List<NodeModel> nodeBySshId = nodeService.getNodeBySshId(ftpModel.getId());
+            ftpModel.setLinkNode(CollUtil.getFirst(nodeBySshId));*/
+        });
+        return new JsonMessage<>(200, "", pageResultDto);
+    }
+
+    @GetMapping(value = "list_data_all.json", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.LIST)
+    public IJsonMessage<List<FtpModel>> listDataAll(HttpServletRequest request) {
+        List<FtpModel> list = ftpService.listByWorkspace(request);
+        return new JsonMessage<>(200, "", list);
+    }
+
+    @GetMapping(value = "list-tree", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.LIST)
+    public IJsonMessage<Tree<String>> listTree(HttpServletRequest request) {
+        List<FtpModel> list = ftpService.listByWorkspace(request);
+        Map<String, TreeNode<String>> groupNode = new HashMap<>(4);
+        List<TreeNode<String>> treeNodes = list.stream()
+            .map(ftpModel -> {
+                String group = ftpModel.getGroup();
+                String groupId = SecureUtil.sha1(StrUtil.emptyToDefault(group, StrUtil.EMPTY));
+                String groupId2 = StrUtil.format("g_{}", groupId);
+                groupNode.computeIfAbsent(groupId, s -> new TreeNode<>(groupId2, StrUtil.SLASH, group, ftpModel.getName()));
+                //
+                TreeNode<String> treeNode = new TreeNode<>(ftpModel.getId(), groupId2, ftpModel.getName(), ftpModel.getName());
+                Map<String, Object> extra = new HashMap<>();
+                extra.put("fileDirs", ftpModel.getFileDirs());
+                extra.put("isLeaf", true);
+                treeNode.setExtra(extra);
+                return treeNode;
+            })
+            .collect(Collectors.toList());
+        //
+        treeNodes.addAll(groupNode.values());
+        Tree<String> tree = TreeUtil.buildSingle(treeNodes, StrUtil.SLASH);
+        return new JsonMessage<>(200, "", tree);
+    }
+
+    @GetMapping(value = "get-item.json", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.LIST)
+    public IJsonMessage<FtpModel> getItem(@ValidatorItem String id, HttpServletRequest request) {
+        FtpModel byKey = ftpService.getByKey(id, request);
+        Assert.notNull(byKey, I18nMessageUtil.get("i18n.ssh_does_not_exist_with_message.de6c"));
+        return new JsonMessage<>(200, "", byKey);
+    }
+
+    /**
+     * 查询所有的分组
+     *
+     * @return list
+     */
+    @GetMapping(value = "list-group-all", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.LIST)
+    public IJsonMessage<List<String>> listGroupAll(HttpServletRequest request) {
+        List<String> listGroup = ftpService.listGroup(request);
+        return JsonMessage.success("", listGroup);
+    }
+
+    /**
+     * 编辑
+     *
+     * @param name    名称
+     * @param group   分组名
+     * @param request 请求对象
+     * @param id      ID
+     * @return json
+     */
+    @PostMapping(value = "save.json", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.EDIT)
+    public IJsonMessage<String> save(@ValidatorItem(value = ValidatorRule.NOT_BLANK, msg = "i18n.parameter_error_ssh_name_cannot_be_empty.ff4f") String name,
+                                     String id,
+                                     String group,
+                                     HttpServletRequest request) {
+        FtpModel ftpModel = new FtpModel();
+        ftpModel.setName(name);
+        ftpModel.setGroup(group);
+        ftpModel.setId(id);
+        ftpService.updateById(ftpModel, request);
+        return JsonMessage.success(I18nMessageUtil.get("i18n.operation_succeeded.3313"));
+    }
+
+
+    @PostMapping(value = "del.json", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.DEL)
+    public IJsonMessage<Object> del(@ValidatorItem(value = ValidatorRule.NOT_BLANK) String id, HttpServletRequest request) {
+        boolean checkSsh = buildInfoService.checkReleaseMethodByLike(id, request, BuildReleaseMethod.Ssh);
+        Assert.state(!checkSsh, I18nMessageUtil.get("i18n.ssh_with_build_items_message.0f6d"));
+        // 判断是否绑定节点
+        List<NodeModel> nodeBySshId = nodeService.getNodeBySshId(id);
+        Assert.state(CollUtil.isEmpty(nodeBySshId), I18nMessageUtil.get("i18n.ssh_bound_to_node_message.7b64"));
+
+        ftpService.delByKey(id, request);
+        //
+        return JsonMessage.success(I18nMessageUtil.get("i18n.operation_succeeded.3313"));
+    }
+
+    @PostMapping(value = "del-fore", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.DEL)
+    @SystemPermission
+    public IJsonMessage<Object> delFore(@ValidatorItem(value = ValidatorRule.NOT_BLANK) String id) {
+        boolean checkSsh = buildInfoService.checkReleaseMethodByLike(id, BuildReleaseMethod.Ssh);
+        Assert.state(!checkSsh, I18nMessageUtil.get("i18n.ssh_with_build_items_message.0f6d"));
+        // 判断是否绑定节点
+        List<NodeModel> nodeBySshId = nodeService.getNodeBySshId(id);
+        Assert.state(CollUtil.isEmpty(nodeBySshId), I18nMessageUtil.get("i18n.ssh_bound_to_node_message.7b64"));
+
+        ftpService.delByKey(id);
+        //
+        return JsonMessage.success(I18nMessageUtil.get("i18n.operation_succeeded.3313"));
+    }
+
+
+    /**
+     * 同步到指定工作空间
+     *
+     * @param ids           节点ID
+     * @param toWorkspaceId 分配到到工作空间ID
+     * @return msg
+     */
+    @GetMapping(value = "sync-to-workspace", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.EDIT)
+    @SystemPermission()
+    public IJsonMessage<Object> syncToWorkspace(@ValidatorItem String ids,
+                                                @ValidatorItem String toWorkspaceId,
+                                                HttpServletRequest request) {
+        String nowWorkspaceId = nodeService.getCheckUserWorkspace(request);
+        //
+        ftpService.checkUserWorkspace(toWorkspaceId);
+        ftpService.syncToWorkspace(ids, nowWorkspaceId, toWorkspaceId);
+        return JsonMessage.success(I18nMessageUtil.get("i18n.operation_succeeded.3313"));
+    }
+}

+ 66 - 0
modules/server/src/main/java/org/dromara/jpom/controller/ftp/FtpFileController.java

@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2019 Of Him Code Technology Studio
+ * Jpom is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ * 			http://license.coscl.org.cn/MulanPSL2
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+package org.dromara.jpom.controller.ftp;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.lang.Opt;
+import cn.hutool.core.util.StrUtil;
+import java.util.List;
+import java.util.function.BiFunction;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.jpom.common.i18n.I18nMessageUtil;
+import org.dromara.jpom.func.assets.controller.BaseFtpFileController;
+import org.dromara.jpom.func.assets.model.MachineFtpModel;
+import org.dromara.jpom.model.data.FtpModel;
+import org.dromara.jpom.permission.ClassFeature;
+import org.dromara.jpom.permission.Feature;
+import org.dromara.jpom.util.FileUtils;
+import org.springframework.util.Assert;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * ftp 文件管理
+ *
+ * @author wxy
+ * @since 2025/06/04
+ */
+@RestController
+@RequestMapping("node/ftp")
+@Feature(cls = ClassFeature.FTP_FILE)
+@Slf4j
+public class FtpFileController extends BaseFtpFileController {
+
+
+    @Override
+    protected <T> T checkConfigPath(String id, BiFunction<MachineFtpModel, ItemConfig, T> function) {
+        FtpModel ftpModel = ftpService.getByKey(id);
+        Assert.notNull(ftpModel, I18nMessageUtil.get("i18n.no_corresponding_ssh.aa68"));
+        MachineFtpModel machineFtpModel = machineFtpServer.getByKey(ftpModel.getMachineFtpId(), false);
+        return function.apply(machineFtpModel, ftpModel);
+    }
+
+    @Override
+    protected <T> T checkConfigPathChildren(String id, String path, String children, BiFunction<MachineFtpModel, ItemConfig, T> function) {
+        FileUtils.checkSlip(path);
+        Opt.ofBlankAble(children).ifPresent(FileUtils::checkSlip);
+
+        FtpModel ftpModel = ftpService.getByKey(id);
+        Assert.notNull(ftpModel, I18nMessageUtil.get("i18n.no_corresponding_ssh.aa68"));
+        List<String> fileDirs = ftpModel.fileDirs();
+        String normalize = FileUtil.normalize(StrUtil.SLASH + path + StrUtil.SLASH);
+        //
+        Assert.state(CollUtil.contains(fileDirs, normalize), I18nMessageUtil.get("i18n.cannot_operate_current_directory.aa3d"));
+        MachineFtpModel machineFtpModel = machineFtpServer.getByKey(ftpModel.getMachineFtpId(), false);
+        return function.apply(machineFtpModel, ftpModel);
+    }
+}

+ 34 - 9
modules/server/src/main/java/org/dromara/jpom/controller/script/ScriptLogController.java

@@ -37,6 +37,7 @@ import org.springframework.web.bind.annotation.RestController;
 
 import javax.servlet.http.HttpServletRequest;
 import java.io.File;
+import java.util.List;
 
 /**
  * @author bwcx_jzy
@@ -51,8 +52,7 @@ public class ScriptLogController extends BaseServerController {
     private final ScriptExecuteLogServer scriptExecuteLogServer;
     private final ScriptServer scriptServer;
 
-    public ScriptLogController(ScriptExecuteLogServer scriptExecuteLogServer,
-                               ScriptServer scriptServer) {
+    public ScriptLogController(ScriptExecuteLogServer scriptExecuteLogServer, ScriptServer scriptServer) {
         this.scriptExecuteLogServer = scriptExecuteLogServer;
         this.scriptServer = scriptServer;
     }
@@ -78,9 +78,7 @@ public class ScriptLogController extends BaseServerController {
      */
     @RequestMapping(value = "del_log", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
     @Feature(method = MethodFeature.DEL)
-    public IJsonMessage<Object> delLog(@ValidatorItem() String id,
-                                       @ValidatorItem() String executeId,
-                                       HttpServletRequest request) {
+    public IJsonMessage<Object> delLog(@ValidatorItem() String id, @ValidatorItem() String executeId, HttpServletRequest request) {
         ScriptModel item = null;
         try {
             item = scriptServer.getByKeyAndGlobal(id, request, "ignore");
@@ -96,6 +94,36 @@ public class ScriptLogController extends BaseServerController {
         return JsonMessage.success(I18nMessageUtil.get("i18n.delete_success.0007"));
     }
 
+    /**
+     * 批量删除日志
+     *
+     * @param ids id+
+     * @return json
+     */
+    @RequestMapping(value = "batch_del_log", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.DEL)
+    public IJsonMessage<Object> delLog(@ValidatorItem() String ids, HttpServletRequest request) {
+        List<String> list = StrUtil.splitTrim(ids, StrUtil.COMMA);
+        ScriptModel item = null;
+        for (String itemId : list) {
+            String[] list1 = StrUtil.splitToArray(itemId, StrUtil.COLON);
+            String id = list1[1];
+            String executeId = list1[0];
+            try {
+                item = scriptServer.getByKeyAndGlobal(id, request, "ignore");
+            } catch (IllegalArgumentException | IllegalStateException e) {
+                if (!StrUtil.equals("ignore", e.getMessage())) {
+                    throw e;
+                }
+            }
+            File logFile = item == null ? ScriptModel.logFile(id, executeId) : item.logFile(executeId);
+            boolean fastDel = CommandUtil.systemFastDel(logFile);
+            Assert.state(!fastDel, I18nMessageUtil.get("i18n.delete_log_file_failure.bf0b"));
+            scriptExecuteLogServer.delByKey(executeId);
+        }
+        return JsonMessage.success(I18nMessageUtil.get("i18n.delete_success.0007"));
+    }
+
     /**
      * 获取的日志
      *
@@ -106,10 +134,7 @@ public class ScriptLogController extends BaseServerController {
      */
     @RequestMapping(value = "log", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
     @Feature(method = MethodFeature.LIST)
-    public IJsonMessage<JSONObject> getNowLog(@ValidatorItem() String id,
-                                              @ValidatorItem() String executeId,
-                                              @ValidatorItem(value = ValidatorRule.POSITIVE_INTEGER, msg = "i18n.line_number_error.c65d") int line,
-                                              HttpServletRequest request) {
+    public IJsonMessage<JSONObject> getNowLog(@ValidatorItem() String id, @ValidatorItem() String executeId, @ValidatorItem(value = ValidatorRule.POSITIVE_INTEGER, msg = "i18n.line_number_error.c65d") int line, HttpServletRequest request) {
         ScriptModel item = scriptServer.getByKey(id, request);
         Assert.notNull(item, I18nMessageUtil.get("i18n.no_data_found.4ffb"));
         File logFile = item.logFile(executeId);

+ 12 - 9
modules/server/src/main/java/org/dromara/jpom/controller/ssh/CommandLogController.java

@@ -10,6 +10,7 @@
 package org.dromara.jpom.controller.ssh;
 
 import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.StrUtil;
 import cn.hutool.extra.servlet.ServletUtil;
 import cn.keepbx.jpom.IJsonMessage;
 import cn.keepbx.jpom.model.JsonMessage;
@@ -77,13 +78,16 @@ public class CommandLogController extends BaseServerController {
     @RequestMapping(value = "del", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
     @Feature(method = MethodFeature.DEL)
     public IJsonMessage<String> del(String id, HttpServletRequest request) {
-        CommandExecLogModel execLogModel = commandExecLogService.getByKey(id, request);
-        Assert.notNull(execLogModel, I18nMessageUtil.get("i18n.no_record.ff41"));
-        File logFile = execLogModel.logFile();
-        boolean fastDel = CommandUtil.systemFastDel(logFile);
-        Assert.state(!fastDel, I18nMessageUtil.get("i18n.log_file_cleanup_failed.3a3b"));
-        //
-        commandExecLogService.delByKey(id);
+        List<String> list = StrUtil.splitTrim(id, StrUtil.COMMA);
+        for (String id1 : list) {
+            CommandExecLogModel execLogModel = commandExecLogService.getByKey(id1, request);
+            Assert.notNull(execLogModel, I18nMessageUtil.get("i18n.no_record.ff41"));
+            File logFile = execLogModel.logFile();
+            boolean fastDel = CommandUtil.systemFastDel(logFile);
+            Assert.state(!fastDel, I18nMessageUtil.get("i18n.log_file_cleanup_failed.3a3b"));
+            //
+            commandExecLogService.delByKey(id1);
+        }
         return JsonMessage.success(I18nMessageUtil.get("i18n.operation_succeeded.3313"));
     }
 
@@ -137,8 +141,7 @@ public class CommandLogController extends BaseServerController {
      */
     @RequestMapping(value = "log", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
     @Feature(method = MethodFeature.LIST)
-    public IJsonMessage<JSONObject> log(@ValidatorItem(value = ValidatorRule.NOT_BLANK, msg = "i18n.no_data.1ac0") String id,
-                                        @ValidatorItem(value = ValidatorRule.POSITIVE_INTEGER, msg = "i18n.line_number_error.c65d") int line, HttpServletRequest request) {
+    public IJsonMessage<JSONObject> log(@ValidatorItem(value = ValidatorRule.NOT_BLANK, msg = "i18n.no_data.1ac0") String id, @ValidatorItem(value = ValidatorRule.POSITIVE_INTEGER, msg = "i18n.line_number_error.c65d") int line, HttpServletRequest request) {
         CommandExecLogModel item = commandExecLogService.getByKey(id, request);
         Assert.notNull(item, I18nMessageUtil.get("i18n.no_data_found.4ffb"));
 

+ 3 - 3
modules/server/src/main/java/org/dromara/jpom/controller/system/BackupInfoController.java

@@ -129,7 +129,7 @@ public class BackupInfoController extends BaseServerController {
             return new JsonMessage<>(400, I18nMessageUtil.get("i18n.backup_file_not_exist.9628"));
         }
         // 清空 sql 加载记录
-        StorageServiceFactory.clearExecuteSqlLog();
+        StorageServiceFactory.getInstance().clearExecuteSqlLog();
         // 还原备份文件
         boolean flag = backupInfoService.restoreWithSql(filePath);
         if (flag) {
@@ -199,7 +199,7 @@ public class BackupInfoController extends BaseServerController {
         String saveFileName = UnicodeUtil.toUnicode(originalFilename);
         saveFileName = saveFileName.replace(StrUtil.BACKSLASH, "_");
         // 存储目录
-        File directory = FileUtil.file(StorageServiceFactory.dbLocalPath(), DbExtConfig.BACKUP_DIRECTORY_NAME);
+        File directory = FileUtil.file(StorageServiceFactory.getInstance().dbLocalPath(), DbExtConfig.BACKUP_DIRECTORY_NAME);
         // 生成唯一id
         String format = String.format("%s_%s", IdUtil.fastSimpleUUID(), saveFileName);
         format = StrUtil.maxLength(format, 40);
@@ -267,7 +267,7 @@ public class BackupInfoController extends BaseServerController {
         Set<Class<?>> classes = ClassUtil.scanPackageByAnnotation("org.dromara.jpom", TableName.class);
         Map<String, String> TABLE_NAME_MAP = CollStreamUtil.toMap(classes, aClass -> {
             TableName tableName = aClass.getAnnotation(TableName.class);
-            return tableName.value();
+            return backupInfoService.parseRealTableName(tableName);
         }, aClass -> {
             TableName tableName = aClass.getAnnotation(TableName.class);
             return I18nMessageUtil.get(tableName.nameKey());

+ 1 - 1
modules/server/src/main/java/org/dromara/jpom/controller/system/SystemConfigController.java

@@ -157,7 +157,7 @@ public class SystemConfigController extends BaseServerController {
             if (!StrUtil.equals(oldDbExtConfigUserName, newDbExtConfigUserName) || !StrUtil.equals(oldDbExtConfigUserPwd, newDbExtConfigUserPwd)) {
                 // 执行修改数据库账号密码
                 Assert.state(restartBool, I18nMessageUtil.get("i18n.modify_db_password_must_restart.d08d"));
-                StorageServiceFactory.get().alterUser(oldDbExtConfigUserName, newDbExtConfigUserName, newDbExtConfigUserPwd);
+                StorageServiceFactory.getInstance().get().alterUser(oldDbExtConfigUserName, newDbExtConfigUserName, newDbExtConfigUserPwd);
             }
         }
         Resource resource = ExtConfigBean.getResource();

+ 11 - 7
modules/server/src/main/java/org/dromara/jpom/controller/system/WorkspaceController.java

@@ -180,14 +180,15 @@ public class WorkspaceController extends BaseServerController {
                 .map(aClass1 -> aClass1 != Void.class ? aClass1 : null)
                 .map(aClass1 -> {
                     TableName tableName1 = aClass1.getAnnotation(TableName.class);
-                    return tableName1.value();
+                    return workspaceService.parseRealTableName(tableName1);
                 })
                 .orElse(StrUtil.EMPTY);
             //
-            String sql = "select  count(1) as cnt from " + tableName.value() + " where workspaceId=?";
+            String value = workspaceService.parseRealTableName(tableName);
+            String sql = "select  count(1) as cnt from " + value + " where workspaceId=?";
             Number number = workspaceService.queryNumber(sql, id);
 
-            TreeNode<String> treeNode = new TreeNode<>(tableName.value(), parent, I18nMessageUtil.get(tableName.nameKey()), 0);
+            TreeNode<String> treeNode = new TreeNode<>(value, parent, I18nMessageUtil.get(tableName.nameKey()), 0);
             //
             JSONObject jsonObject = new JSONObject();
             jsonObject.put("workspaceBind", tableName.workspaceBind());
@@ -229,14 +230,16 @@ public class WorkspaceController extends BaseServerController {
                 TableName tableName1 = parents.getAnnotation(TableName.class);
                 Assert.notNull(tableName1, I18nMessageUtil.get("i18n.parent_table_info_config_error.2f52") + aClass);
                 //
-                String sql = "select  count(1) as cnt from " + tableName1.value() + " where workspaceId=?";
+                String value = workspaceService.parseRealTableName(tableName1);
+                String sql = "select  count(1) as cnt from " + value + " where workspaceId=?";
                 int cnt = ObjectUtil.defaultIfNull(workspaceService.queryNumber(sql, id), Number::intValue, 0);
                 Assert.state(cnt <= 0, StrUtil.format(I18nMessageUtil.get("i18n.associated_data_and_exist_in_workspace.5fa7"), I18nMessageUtil.get(tableName.nameKey()), I18nMessageUtil.get(tableName1.nameKey())));
                 // 等待自动删除
                 autoDeleteClass.add(aClass);
             } else {
                 // 其他严格检查的情况
-                String sql = "select  count(1) as cnt from " + tableName.value() + " where workspaceId=?";
+                String value = workspaceService.parseRealTableName(tableName);
+                String sql = "select  count(1) as cnt from " + value + " where workspaceId=?";
                 int cnt = ObjectUtil.defaultIfNull(workspaceService.queryNumber(sql, id), Number::intValue, 0);
                 Assert.state(cnt <= 0, I18nMessageUtil.get("i18n.associated_data_exists_in_workspace.8827") + I18nMessageUtil.get(tableName.nameKey()));
             }
@@ -249,10 +252,11 @@ public class WorkspaceController extends BaseServerController {
         for (Class<?> aClass : autoDeleteClass) {
             TableName tableName = aClass.getAnnotation(TableName.class);
             // 自动删除
-            String sql = "delete from " + tableName.value() + " where workspaceId=?";
+            String value = workspaceService.parseRealTableName(tableName);
+            String sql = "delete from " + value + " where workspaceId=?";
             int execute = workspaceService.execute(sql, id);
             if (execute > 0) {
-                autoDelete.append(StrUtil.format(I18nMessageUtil.get("i18n.auto_delete_data.ca62"), tableName.value(), execute));
+                autoDelete.append(StrUtil.format(I18nMessageUtil.get("i18n.auto_delete_data.ca62"), value, execute));
             }
         }
         // 删除缓存

+ 635 - 0
modules/server/src/main/java/org/dromara/jpom/func/assets/controller/BaseFtpFileController.java

@@ -0,0 +1,635 @@
+/*
+ * Copyright (c) 2019 Of Him Code Technology Studio
+ * Jpom is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ * 			http://license.coscl.org.cn/MulanPSL2
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+package org.dromara.jpom.func.assets.controller;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.exceptions.ExceptionUtil;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.CharsetUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.core.util.URLUtil;
+import cn.hutool.crypto.SecureUtil;
+import cn.hutool.extra.ftp.Ftp;
+import cn.hutool.extra.servlet.ServletUtil;
+import cn.keepbx.jpom.IJsonMessage;
+import cn.keepbx.jpom.model.JsonMessage;
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.jcraft.jsch.SftpException;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+import java.util.TimeZone;
+import java.util.function.BiFunction;
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import lombok.Lombok;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.net.ftp.FTPFile;
+import org.dromara.jpom.common.BaseServerController;
+import org.dromara.jpom.common.i18n.I18nMessageUtil;
+import org.dromara.jpom.common.validator.ValidatorItem;
+import org.dromara.jpom.func.assets.model.MachineFtpModel;
+import org.dromara.jpom.func.assets.server.MachineFtpServer;
+import org.dromara.jpom.model.data.AgentWhitelist;
+import org.dromara.jpom.permission.Feature;
+import org.dromara.jpom.permission.MethodFeature;
+import org.dromara.jpom.service.node.ftp.FtpService;
+import org.dromara.jpom.system.ServerConfig;
+import org.dromara.jpom.util.CommandUtil;
+import org.dromara.jpom.util.CompressionFileUtil;
+import org.dromara.jpom.util.StringUtil;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.multipart.MultipartFile;
+
+/**
+ * @author wxyShine
+ * @since 2025/05/28
+ * 临时测试ftp服务器 https://sftpcloud.io/tools/free-ftp-server
+ */
+@Slf4j
+public abstract class BaseFtpFileController extends BaseServerController {
+
+    @Resource
+    protected FtpService ftpService;
+    @Resource
+    protected MachineFtpServer machineFtpServer;
+    @Resource
+    private ServerConfig serverConfig;
+
+    public interface ItemConfig {
+        /**
+         * 允许编辑的文件后缀
+         *
+         * @return 文件后缀
+         */
+        List<String> allowEditSuffix();
+
+        /**
+         * 允许管理的文件目录
+         *
+         * @return 文件目录
+         */
+        List<String> fileDirs();
+    }
+
+    /**
+     * 验证数据id 和目录合法性
+     *
+     * @param id       数据id
+     * @param function 回调
+     * @param <T>      泛型
+     * @return 处理后的数据
+     */
+    protected abstract <T> T checkConfigPath(String id, BiFunction<MachineFtpModel, ItemConfig, T> function);
+
+    /**
+     * 验证数据id 和目录合法性
+     *
+     * @param id              数据id
+     * @param allowPathParent 想要验证的目录 (授权)
+     * @param nextPath        授权后的二级路径
+     * @param function        回调
+     * @param <T>             泛型
+     * @return 处理后的数据
+     */
+    protected abstract <T> T checkConfigPathChildren(String id, String allowPathParent, String nextPath, BiFunction<MachineFtpModel, ItemConfig, T> function);
+
+    @RequestMapping(value = "download", method = RequestMethod.GET)
+    @Feature(method = MethodFeature.DOWNLOAD)
+    public void download(@ValidatorItem String id,
+                         @ValidatorItem String allowPathParent,
+                         @ValidatorItem String nextPath,
+                         @ValidatorItem String name,
+                         HttpServletResponse response) throws IOException {
+        MachineFtpModel machineFtpModel = this.checkConfigPathChildren(id, allowPathParent, nextPath, (machineFtpModel1, itemConfig) -> machineFtpModel1);
+        if (machineFtpModel == null) {
+            ServletUtil.write(response, I18nMessageUtil.get("i18n.ssh_error_or_folder_not_configured.c087"), MediaType.TEXT_HTML_VALUE);
+            return;
+        }
+        this.downloadFile(machineFtpModel, allowPathParent, nextPath, name, response);
+    }
+
+    /**
+     * 根据 id 获取 fileDirs 目录集合
+     *
+     * @param id ftp id
+     * @return json
+     * @author Hotstrip
+     * @since for dev 3.x
+     */
+    @RequestMapping(value = "root_file_data.json", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.LIST)
+    public IJsonMessage<JSONArray> rootFileList(@ValidatorItem String id) {
+        //
+        return this.checkConfigPath(id, (machineFtpModel, itemConfig) -> {
+            JSONArray listDir = listRootDir(machineFtpModel, itemConfig.fileDirs());
+            return JsonMessage.success("", listDir);
+        });
+    }
+
+
+    @RequestMapping(value = "list_file_data.json", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.LIST)
+    public IJsonMessage<JSONArray> listData(@ValidatorItem String id,
+                                            @ValidatorItem String allowPathParent,
+                                            @ValidatorItem String nextPath) {
+        return this.checkConfigPathChildren(id, allowPathParent, nextPath, (machineFtpModel, itemConfig) -> {
+            try {
+                JSONArray listDir = listDir(machineFtpModel, allowPathParent, nextPath, itemConfig);
+                return JsonMessage.success("", listDir);
+            } catch (SftpException e) {
+                throw Lombok.sneakyThrow(e);
+            }
+        });
+    }
+
+    @RequestMapping(value = "read_file_data.json", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.LIST)
+    public IJsonMessage<String> readFileData(@ValidatorItem String id,
+                                             @ValidatorItem String allowPathParent,
+                                             @ValidatorItem String nextPath,
+                                             @ValidatorItem String name) {
+        return this.checkConfigPathChildren(id, allowPathParent, nextPath, (machineFtpModel, itemConfig) -> {
+
+            List<String> allowEditSuffix = itemConfig.allowEditSuffix();
+            Charset charset = AgentWhitelist.checkFileSuffix(allowEditSuffix, name);
+            //
+            String content = this.readFile(machineFtpModel, allowPathParent, nextPath, name, charset);
+            return JsonMessage.success("", content);
+        });
+    }
+
+    @RequestMapping(value = "update_file_data.json", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.EDIT)
+    public IJsonMessage<String> updateFileData(@ValidatorItem String id,
+                                               @ValidatorItem String allowPathParent,
+                                               @ValidatorItem String nextPath,
+                                               @ValidatorItem String name,
+                                               @ValidatorItem String content) {
+        return this.checkConfigPathChildren(id, allowPathParent, nextPath, (machineFtpModel, itemConfig) -> {
+            //
+            List<String> allowEditSuffix = itemConfig.allowEditSuffix();
+            Charset charset = AgentWhitelist.checkFileSuffix(allowEditSuffix, name);
+            // 缓存到本地
+            File file = FileUtil.file(serverConfig.getUserTempPath(), machineFtpModel.getId(), allowPathParent, nextPath, name);
+            try {
+                FileUtil.writeString(content, file, charset);
+                // 上传
+                this.syncFile(machineFtpModel, allowPathParent, nextPath, name, file);
+            } finally {
+                //
+                FileUtil.del(file);
+            }
+            //
+            return JsonMessage.success(I18nMessageUtil.get("i18n.modify_success.69be"));
+        });
+    }
+
+    /**
+     * 读取文件
+     *
+     * @param machineFtpModel ftp
+     * @param allowPathParent 路径
+     * @param nextPath        二级路径
+     * @param name            文件
+     * @param charset         编码格式
+     */
+    private String readFile(MachineFtpModel machineFtpModel, String allowPathParent, String nextPath, String name, Charset charset) {
+        String normalize = FileUtil.normalize(allowPathParent + StrUtil.SLASH + nextPath + StrUtil.SLASH + name);
+
+        try (Ftp ftp = machineFtpServer.getFtpClient(machineFtpModel);
+             InputStream inputStream = ftp.getClient().retrieveFileStream(normalize);
+             ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
+
+            if (inputStream == null) {
+                throw new RuntimeException(I18nMessageUtil.get("i18n.file_not_exist_or_unable_to_open.b045") + normalize);
+            }
+
+            IoUtil.copy(inputStream, outputStream);
+            ftp.getClient().completePendingCommand();
+
+            return outputStream.toString(charset.name());
+
+        } catch (IOException e) {
+            throw new RuntimeException(I18nMessageUtil.get("i18n.ftp_read_file_failed.e738"), e);
+        }
+    }
+
+    /**
+     * 上传文件
+     *
+     * @param machineFtpModel ftp
+     * @param allowPathParent 路径
+     * @param nextPath        二级路径
+     * @param name            文件
+     * @param file            同步上传文件
+     */
+    private void syncFile(MachineFtpModel machineFtpModel,
+                          String allowPathParent,
+                          String nextPath,
+                          String name,
+                          File file) {
+        String normalizeDir = FileUtil.normalize(allowPathParent + StrUtil.SLASH + nextPath);
+
+        try (Ftp ftp = machineFtpServer.getFtpClient(machineFtpModel)) {
+            // 直接upload到原来目录 会覆盖更新文件
+            boolean success = ftp.upload(normalizeDir, file);
+            if (!success) {
+                throw new RuntimeException(I18nMessageUtil.get("i18n.ftp_upload_failed.8298"));
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(I18nMessageUtil.get("i18n.ftp_connection_or_operation_exception.09af"), e);
+        }
+    }
+
+    /**
+     * 下载文件
+     *
+     * @param machineFtpModel ftp
+     * @param allowPathParent 路径
+     * @param name            文件
+     * @param response        响应
+     * @throws IOException   io
+     * @throws SftpException sftp
+     */
+    private void downloadFile(MachineFtpModel machineFtpModel, String allowPathParent, String nextPath, String name, HttpServletResponse response) throws IOException {
+        final String charset = ObjectUtil.defaultIfNull(response.getCharacterEncoding(), CharsetUtil.UTF_8);
+        String fileName = FileUtil.getName(name);
+
+        response.setHeader("Content-Disposition", StrUtil.format("attachment;filename={}", URLUtil.encode(fileName, Charset.forName(charset))));
+        response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+
+        String normalize = FileUtil.normalize(allowPathParent + StrUtil.SLASH + nextPath + StrUtil.SLASH + name);
+
+        InputStream inputStream = null;
+        try (Ftp ftp = machineFtpServer.getFtpClient(machineFtpModel)) {
+            inputStream = ftp.getClient().retrieveFileStream(normalize);
+
+            if (inputStream == null) {
+                throw new RuntimeException(I18nMessageUtil.get("i18n.file_not_exist_or_unable_to_download.b977") + normalize);
+            }
+
+            IoUtil.copy(inputStream, response.getOutputStream());
+            ftp.getClient().completePendingCommand(); // 关键!
+
+        } catch (IOException e) {
+            throw new RuntimeException(I18nMessageUtil.get("i18n.ftp_download_file_failed.2e42"), e);
+        } finally {
+            IoUtil.close(inputStream);
+        }
+    }
+
+    /**
+     * 查询文件夹下所有文件
+     *
+     * @param ftpModel        ssh
+     * @param allowPathParent 允许的路径
+     * @param nextPath        下 N 级的文件夹
+     * @return array
+     * @throws SftpException sftp
+     */
+    @SuppressWarnings("unchecked")
+    private JSONArray listDir(MachineFtpModel ftpModel, String allowPathParent, String nextPath, ItemConfig itemConfig) throws SftpException {
+
+        List<String> allowEditSuffix = itemConfig.allowEditSuffix();
+        try (Ftp ftp = machineFtpServer.getFtpClient(ftpModel)) {
+            String children2 = StrUtil.emptyToDefault(nextPath, StrUtil.SLASH);
+            String allPath = StrUtil.format("{}/{}", allowPathParent, children2);
+            allPath = FileUtil.normalize(allPath);
+            JSONArray jsonArray = new JSONArray();
+            FTPFile[] ftpFiles;
+            try {
+                ftpFiles = ftp.lsFiles(allPath);
+            } catch (Exception e) {
+                log.warn(I18nMessageUtil.get("i18n.get_folder_failure.0fda"), e);
+                Throwable causedBy = ExceptionUtil.getCausedBy(e, SftpException.class);
+                if (causedBy != null) {
+                    throw new IllegalStateException(I18nMessageUtil.get("i18n.ftp_folder_query_failed.0011") + causedBy.getMessage());
+                }
+                throw new IllegalStateException(I18nMessageUtil.get("i18n.query_folder_failed.3f0e") + e.getMessage());
+            }
+            for (FTPFile file : ftpFiles) {
+                String filename = file.getName();
+                if (StrUtil.DOT.equals(filename) || StrUtil.DOUBLE_DOT.equals(filename)) {
+                    continue;
+                }
+                JSONObject jsonObject = new JSONObject();
+                jsonObject.put("name", filename);
+                jsonObject.put("id", SecureUtil.sha1(allPath + StrUtil.SLASH + filename));
+
+                Calendar timestamp = file.getTimestamp();
+                timestamp.setTimeZone(TimeZone.getTimeZone("UTC"));
+                jsonObject.put("modifyTime", timestamp);
+
+                jsonObject.put("dir", file.isDirectory());
+                jsonObject.put("size", file.getSize());
+                jsonObject.put("textFileEdit", AgentWhitelist.checkSilentFileSuffix(allowEditSuffix, filename));
+                jsonObject.put("longname", file.toFormattedString(TimeZone.getDefault().toString()));
+                jsonObject.put("link", file.getLink());
+                jsonObject.put("permissions", getPermissionString(file));
+                jsonObject.put("allowPathParent", allowPathParent);
+                jsonObject.put("nextPath", FileUtil.normalize(children2));
+                jsonArray.add(jsonObject);
+            }
+            return jsonArray;
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 列出目前,判断是否存在
+     *
+     * @param ftpModel 数据信息
+     * @param list     目录
+     * @return Array
+     */
+    private JSONArray listRootDir(MachineFtpModel ftpModel, List<String> list) {
+        JSONArray jsonArray = new JSONArray();
+        if (CollUtil.isEmpty(list)) {
+            return jsonArray;
+        }
+
+        try (Ftp ftp = machineFtpServer.getFtpClient(ftpModel)) {
+            for (String allowPathParent : list) {
+                JSONObject jsonObject = new JSONObject();
+                jsonObject.put("id", SecureUtil.sha1(allowPathParent));
+                jsonObject.put("allowPathParent", allowPathParent);
+                ftp.ls(allowPathParent);
+                jsonArray.add(jsonObject);
+            }
+        } catch (Exception e) {
+            log.error(I18nMessageUtil.get("i18n.ftp_connection_failed.1f2f"), e);
+        }
+        return jsonArray;
+    }
+
+
+    @RequestMapping(value = "delete.json", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.DEL)
+    public IJsonMessage<String> delete(@ValidatorItem String id,
+                                       @ValidatorItem String allowPathParent,
+                                       @ValidatorItem String nextPath,
+                                       String name) {
+        // name 可能为空,为空情况是删除目录
+        String name2 = StrUtil.emptyToDefault(name, StrUtil.EMPTY);
+        Assert.state(!StrUtil.equals(name2, StrUtil.SLASH), I18nMessageUtil.get("i18n.cannot_delete_root_dir.fcdc"));
+        return this.checkConfigPathChildren(id, allowPathParent, nextPath, (machineFtpModel, itemConfig) -> {
+
+            try (Ftp ftp = machineFtpServer.getFtpClient(machineFtpModel)) {
+                String normalize = FileUtil.normalize(allowPathParent + StrUtil.SLASH + nextPath + StrUtil.SLASH + name2);
+                Assert.state(!StrUtil.equals(normalize, StrUtil.SLASH), I18nMessageUtil.get("i18n.cannot_delete_root_dir.fcdc"));
+                // 尝试删除
+                boolean dirOrFile = this.tryDelDirOrFile(ftp, normalize);
+                if (dirOrFile) {
+                    String parent = FileUtil.getParent(normalize, 1);
+                    return JsonMessage.success(I18nMessageUtil.get("i18n.delete_success.0007"), parent);
+                }
+                return JsonMessage.success(I18nMessageUtil.get("i18n.delete_success.0007"));
+            } catch (Exception e) {
+                log.error(I18nMessageUtil.get("i18n.ssh_file_deletion_exception.5ba5"), e);
+                return new JsonMessage<>(400, I18nMessageUtil.get("i18n.delete_failure_with_colon.b429") + e.getMessage());
+            }
+        });
+    }
+
+    @RequestMapping(value = "rename.json", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.EDIT)
+    public IJsonMessage<String> rename(@ValidatorItem String id,
+                                       @ValidatorItem String allowPathParent,
+                                       @ValidatorItem String nextPath,
+                                       @ValidatorItem String name,
+                                       @ValidatorItem String newname) {
+
+        return this.checkConfigPathChildren(id, allowPathParent, nextPath, (machineFtpModel, itemConfig) -> {
+
+            try (Ftp ftp = machineFtpServer.getFtpClient(machineFtpModel)) {
+                String oldPath = FileUtil.normalize(allowPathParent + StrUtil.SLASH + nextPath + StrUtil.SLASH + name);
+                String newPath = FileUtil.normalize(allowPathParent + StrUtil.SLASH + nextPath + StrUtil.SLASH + newname);
+                ftp.getClient().rename(oldPath, newPath);
+            } catch (Exception e) {
+                log.error(I18nMessageUtil.get("i18n.ftp_rename_failed_exception.0fcc"), e);
+                return new JsonMessage<>(400, I18nMessageUtil.get("i18n.ftp_rename_failed_exception.0fcc") + e.getMessage());
+            }
+            return JsonMessage.success(I18nMessageUtil.get("i18n.operation_succeeded.3313"));
+        });
+    }
+
+    /**
+     * 删除文件 或者 文件夹
+     *
+     * @param ftp  ftp
+     * @param path 路径
+     * @return true 删除的是 文件夹
+     */
+    private boolean tryDelDirOrFile(Ftp ftp, String path) {
+        try {
+            // 先尝试删除文件夹
+            ftp.delDir(path);
+            return true;
+        } catch (Exception e) {
+            // 删除文件
+            ftp.delFile(path);
+        }
+        return false;
+    }
+
+    /**
+     * 上传分片
+     *
+     * @param file       文件对象
+     * @param sliceId    分片id
+     * @param totalSlice 总分片
+     * @param nowSlice   当前分片
+     * @param fileSumMd5 文件 md5
+     * @return json
+     */
+   /* @PostMapping(value = "upload-sharding", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.UPLOAD, log = false)
+    public IJsonMessage<String> uploadSharding(MultipartFile file,
+                                               String sliceId,
+                                               Integer totalSlice,
+                                               Integer nowSlice,
+                                               String fileSumMd5,
+                                               @ValidatorItem String id,
+                                               @ValidatorItem String allowPathParent,
+                                               @ValidatorItem String nextPath) {
+        return this.checkConfigPathChildren(id, allowPathParent, nextPath, (machineSshModel, itemConfig) -> {
+            String remotePath = FileUtil.normalize(allowPathParent + StrUtil.SLASH + nextPath);
+            Session session = null;
+            ChannelSftp channel = null;
+            try {
+                session = sshService.getSessionByModel(machineSshModel);
+                channel = (ChannelSftp) JschUtil.openChannel(session, ChannelType.SFTP);
+                channel.cd(remotePath);
+                String originalFilename = file.getOriginalFilename();
+                // xxxx.txt.1
+                originalFilename = StrUtil.subBefore(originalFilename, ".", true);
+                if (nowSlice == 0) {
+                    channel.put(file.getInputStream(), originalFilename, ChannelSftp.OVERWRITE);
+                } else {
+                    channel.put(file.getInputStream(), originalFilename, ChannelSftp.APPEND);
+                }
+            } catch (Exception e) {
+                log.error(I18nMessageUtil.get("i18n.ssh_file_upload_exception.5c1c"), e);
+                return new JsonMessage<>(400, I18nMessageUtil.get("i18n.upload_failed.b019") + e.getMessage());
+            } finally {
+                JschUtil.close(channel);
+                JschUtil.close(session);
+            }
+            return JsonMessage.success(I18nMessageUtil.get("i18n.upload_success.a769"));
+        });
+    }
+*/
+    @RequestMapping(value = "upload", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.UPLOAD)
+    public IJsonMessage<String> upload(@ValidatorItem String id,
+                                       @ValidatorItem String allowPathParent,
+                                       @ValidatorItem String nextPath,
+                                       String unzip,
+                                       MultipartFile file) {
+        return this.checkConfigPathChildren(id, allowPathParent, nextPath, (machineFtpModel, itemConfig) -> {
+
+            String remotePath = FileUtil.normalize(allowPathParent + StrUtil.SLASH + nextPath);
+            File filePath = null;
+            File tempUnzipPath = null;
+            try (Ftp ftp = machineFtpServer.getFtpClient(machineFtpModel)) {
+                // 保存路径
+                File tempPath = serverConfig.getUserTempPath();
+                File savePath = FileUtil.file(tempPath, "ftp", machineFtpModel.getId());
+                FileUtil.mkdir(savePath);
+                String originalFilename = file.getOriginalFilename();
+                filePath = FileUtil.file(savePath, originalFilename);
+                //
+                if (Convert.toBool(unzip, false)) {
+                    String extName = FileUtil.extName(originalFilename);
+                    Assert.state(StrUtil.containsAnyIgnoreCase(extName, StringUtil.PACKAGE_EXT), I18nMessageUtil.get("i18n.file_type_not_supported2.d497") + extName);
+                    file.transferTo(filePath);
+                    // 解压
+                    tempUnzipPath = FileUtil.file(savePath, IdUtil.fastSimpleUUID());
+
+                    FileUtil.mkdir(tempUnzipPath);
+                    CompressionFileUtil.unCompress(filePath, tempUnzipPath);
+                    ftp.uploadFileOrDirectory(remotePath, tempUnzipPath);
+                } else {
+                    file.transferTo(filePath);
+                    ftp.uploadFileOrDirectory(remotePath, filePath);
+                }
+            } catch (Exception e) {
+                log.error(I18nMessageUtil.get("i18n.ftp_upload_file_exception.118c"), e);
+                return new JsonMessage<>(400, I18nMessageUtil.get("i18n.ftp_upload_file_exception.118c") + e.getMessage());
+            } finally {
+                CommandUtil.systemFastDel(filePath);
+                CommandUtil.systemFastDel(tempUnzipPath);
+            }
+            return JsonMessage.success(I18nMessageUtil.get("i18n.operation_succeeded.3313"));
+        });
+    }
+
+    /**
+     * @return json
+     * @api {post} new_file_folder.json ssh 中创建文件夹/文件
+     * @apiGroup ftp
+     * @apiUse defResultJson
+     * @apiParam {String} id ftp id
+     * @apiParam {String} path ftp 选择到目录
+     * @apiParam {String} name 文件名
+     * @apiParam {String} unFolder true/1 为文件夹,false/0 为文件
+     * @apiSuccess {JSON}  data
+     */
+    @RequestMapping(value = "new_file_folder.json", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.UPLOAD)
+    public IJsonMessage<String> newFileFolder(String id,
+                                              @ValidatorItem String allowPathParent,
+                                              @ValidatorItem String nextPath,
+                                              @ValidatorItem String name, String unFolder) {
+        Assert.state(!StrUtil.contains(name, StrUtil.SLASH), I18nMessageUtil.get("i18n.file_name_error_message.7a25"));
+        return this.checkConfigPathChildren(id, allowPathParent, nextPath, (machineFtpModel, itemConfig) -> {
+
+            try (Ftp ftp = machineFtpServer.getFtpClient(machineFtpModel)) {
+                String remotePath = FileUtil.normalize(allowPathParent + StrUtil.SLASH + nextPath + StrUtil.SLASH + name);
+
+                File filePath = null;
+                try {
+                    if (ftp.exist(remotePath)) {
+                        return new JsonMessage<>(400, I18nMessageUtil.get("i18n.file_exists.145b"));
+                    }
+                    if (Convert.toBool(unFolder, false)) {
+                        // 创建空文件到临时保存路径
+                        File tempPath = serverConfig.getUserTempPath();
+                        File savePath = FileUtil.file(tempPath, "ftp", machineFtpModel.getId());
+                        FileUtil.mkdir(savePath);
+                        filePath = FileUtil.file(savePath, name);
+                        FileUtil.touch(filePath);
+
+                        // 上传文件到ftp服务器 (需去掉目录中的文件名 防止创建文件同名目录)
+                        ftp.upload(StrUtil.removeAll(remotePath, StrUtil.SLASH + name), filePath);
+
+                    } else {
+                        // 目录
+                        try {
+                            if (ftp.mkdir(remotePath)) {
+                                // 创建成功
+                                return JsonMessage.success(I18nMessageUtil.get("i18n.operation_succeeded.3313"));
+                            }
+                        } catch (Exception e) {
+                            log.error(I18nMessageUtil.get("i18n.ftp_create_folder_exception.a4fe"), e);
+                            return new JsonMessage<>(500, I18nMessageUtil.get("i18n.ftp_create_folder_exception.a4fe") + e.getMessage());
+                        }
+                    }
+                    List<String> result = new ArrayList<>();
+                    return JsonMessage.success(I18nMessageUtil.get("i18n.operation_succeeded_with_details.c773") + CollUtil.join(result, StrUtil.LF));
+                } catch (Exception e) {
+                    throw new RuntimeException(e);
+                } finally {
+                    // 删除临时文件
+                    CommandUtil.systemFastDel(filePath);
+                }
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+        });
+    }
+
+
+    public String getPermissionString(FTPFile file) {
+
+        // 否则按照 Unix 风格格式化
+        StringBuilder sb = new StringBuilder();
+        sb.append(file.isDirectory() ? 'd' : '-');
+        sb.append(file.hasPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION) ? 'r' : '-');
+        sb.append(file.hasPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION) ? 'w' : '-');
+        sb.append(file.hasPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION) ? 'x' : '-');
+
+        sb.append(file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.READ_PERMISSION) ? 'r' : '-');
+        sb.append(file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.WRITE_PERMISSION) ? 'w' : '-');
+        sb.append(file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.EXECUTE_PERMISSION) ? 'x' : '-');
+
+        sb.append(file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.READ_PERMISSION) ? 'r' : '-');
+        sb.append(file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.WRITE_PERMISSION) ? 'w' : '-');
+        sb.append(file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.EXECUTE_PERMISSION) ? 'x' : '-');
+
+        return sb.toString();
+    }
+}

+ 457 - 0
modules/server/src/main/java/org/dromara/jpom/func/assets/controller/MachineFtpController.java

@@ -0,0 +1,457 @@
+package org.dromara.jpom.func.assets.controller;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.date.DatePattern;
+import cn.hutool.core.date.DateTime;
+import cn.hutool.core.io.CharsetDetector;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.lang.Opt;
+import cn.hutool.core.net.NetUtil;
+import cn.hutool.core.text.StrSplitter;
+import cn.hutool.core.text.csv.CsvData;
+import cn.hutool.core.text.csv.CsvReadConfig;
+import cn.hutool.core.text.csv.CsvReader;
+import cn.hutool.core.text.csv.CsvRow;
+import cn.hutool.core.text.csv.CsvUtil;
+import cn.hutool.core.text.csv.CsvWriter;
+import cn.hutool.core.util.EnumUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.db.Entity;
+import cn.hutool.extra.ftp.Ftp;
+import cn.hutool.extra.ftp.FtpMode;
+import cn.hutool.extra.servlet.ServletUtil;
+import cn.keepbx.jpom.IJsonMessage;
+import cn.keepbx.jpom.model.JsonMessage;
+import java.io.File;
+import java.io.IOException;
+import java.io.Reader;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.jpom.common.i18n.I18nMessageUtil;
+import org.dromara.jpom.common.interceptor.PermissionInterceptor;
+import org.dromara.jpom.common.validator.ValidatorItem;
+import org.dromara.jpom.common.validator.ValidatorRule;
+import org.dromara.jpom.configuration.AssetsConfig;
+import org.dromara.jpom.dialect.DialectUtil;
+import org.dromara.jpom.func.BaseGroupNameController;
+import org.dromara.jpom.func.assets.model.MachineFtpModel;
+import org.dromara.jpom.func.assets.server.MachineFtpServer;
+import org.dromara.jpom.model.PageResultDto;
+import org.dromara.jpom.model.data.AgentWhitelist;
+import org.dromara.jpom.model.data.FtpModel;
+import org.dromara.jpom.model.data.WorkspaceModel;
+import org.dromara.jpom.model.user.UserModel;
+import org.dromara.jpom.permission.ClassFeature;
+import org.dromara.jpom.permission.Feature;
+import org.dromara.jpom.permission.MethodFeature;
+import org.dromara.jpom.permission.SystemPermission;
+import org.dromara.jpom.service.node.ftp.FtpService;
+import org.dromara.jpom.service.system.WorkspaceService;
+import org.dromara.jpom.system.ServerConfig;
+import org.springframework.http.MediaType;
+import org.springframework.util.Assert;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+/**
+ * @author bwcx_jzy
+ * @since 2024/8/31
+ */
+@RestController
+@RequestMapping(value = "/system/assets/ftp")
+@Feature(cls = ClassFeature.SYSTEM_ASSETS_MACHINE_SSH)
+@SystemPermission
+@Slf4j
+public class MachineFtpController extends BaseGroupNameController {
+
+    private final MachineFtpServer machineFtpServer;
+    private final AssetsConfig.FtpConfig ftpConfig;
+    private final WorkspaceService workspaceService;
+    private final FtpService ftpService;
+    private final ServerConfig serverConfig;
+
+
+    public MachineFtpController(MachineFtpServer machineFtpServer, AssetsConfig assetsConfig, WorkspaceService workspaceService, FtpService ftpService, ServerConfig serverConfig) {
+        super(machineFtpServer);
+        this.machineFtpServer = machineFtpServer;
+        this.ftpConfig = assetsConfig.getFtp();
+        this.workspaceService = workspaceService;
+        this.ftpService = ftpService;
+        this.serverConfig = serverConfig;
+
+    }
+
+    @PostMapping(value = "list-data", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.LIST)
+    public IJsonMessage<PageResultDto<MachineFtpModel>> listJson(HttpServletRequest request) {
+        PageResultDto<MachineFtpModel> pageResultDto = machineFtpServer.listPage(request);
+        return JsonMessage.success("", pageResultDto);
+    }
+
+    @Override
+    @GetMapping(value = "list-group", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.LIST)
+    public IJsonMessage<Collection<String>> listGroup() {
+        Collection<String> list = dbService.listGroupName();
+        // 合并配置禁用分组
+        List<String> monitorGroupName = ftpConfig.getDisableMonitorGroupName();
+        if (monitorGroupName != null) {
+            list.addAll(monitorGroupName);
+            //
+            list.remove("*");
+            list = new HashSet<>(list);
+        }
+        return JsonMessage.success("", list);
+    }
+
+    /**
+     * 编辑
+     *
+     * @param name               名称
+     * @param host               主机
+     * @param user               用户名
+     * @param password           密码
+     * @param serverLanguageCode 服务器语言
+     * @param systemKey          服务器系统关键词
+     * @param port               端口
+     * @param charset            编码格式
+     * @param id                 ID
+     * @return json
+     */
+    @PostMapping(value = "edit", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.EDIT)
+    public IJsonMessage<String> save(@ValidatorItem(value = ValidatorRule.NOT_BLANK, msg = "i18n.parameter_error_ssh_name_cannot_be_empty.ff4f") String name,
+                                     @ValidatorItem(value = ValidatorRule.NOT_BLANK, msg = "i18n.host_cannot_be_empty.644a") String host,
+                                     @ValidatorItem(value = ValidatorRule.NOT_BLANK, msg = "i18n.parameter_error_user_cannot_be_empty.9239") String user,
+                                     String password,
+                                     @ValidatorItem(value = ValidatorRule.POSITIVE_INTEGER, msg = "i18n.parameter_error_port_error.810d") int port,
+                                     String charset,
+                                     String id,
+                                     Integer timeout,
+                                     String allowEditSuffix,
+                                     String serverLanguageCode,
+                                     String systemKey,
+                                     String mode,
+                                     String groupName) {
+        boolean add = StrUtil.isEmpty(id);
+        if (add) {
+            Assert.hasText(password, I18nMessageUtil.get("i18n.login_password_required.9605"));
+        } else {
+            boolean exists = machineFtpServer.exists(new MachineFtpModel(id));
+            Assert.state(exists, I18nMessageUtil.get("i18n.ftp_not_exist.f9b3"));
+        }
+
+        MachineFtpModel model = new MachineFtpModel();
+        model.setId(id);
+        model.setGroupName(groupName);
+        model.setHost(host);
+        model.setServerLanguageCode(serverLanguageCode);
+        model.setMode(mode);
+        model.setSystemKey(systemKey);
+        // 如果密码传递不为空就设置值 因为上面已经判断了只有修改的情况下 password 才可能为空
+        Opt.ofBlankAble(password).ifPresent(model::setPassword);
+
+        // 获取允许编辑的后缀
+        List<String> allowEditSuffixList = AgentWhitelist.parseToList(allowEditSuffix, I18nMessageUtil.get("i18n.suffix_cannot_be_empty.ec72"));
+        model.allowEditSuffix(allowEditSuffixList);
+        model.setPort(port);
+        model.setUser(user);
+        model.setName(name);
+        model.setTimeout(timeout);
+        try {
+            Charset.forName(charset);
+            model.setCharset(charset);
+        } catch (Exception e) {
+            return new JsonMessage<>(405, I18nMessageUtil.get("i18n.correct_encoding_format_required.1f7f") + e.getMessage());
+        }
+        // 判断重复
+        Entity entity = Entity.create();
+        entity.set("host", model.getHost());
+        entity.set("port", model.getPort());
+        entity.set(DialectUtil.wrapField("user"), model.getUser());
+        Opt.ofBlankAble(id).ifPresent(s -> entity.set("id", StrUtil.format(" <> {}", s)));
+        boolean exists = machineFtpServer.exists(entity);
+        Assert.state(!exists, I18nMessageUtil.get("i18n.ftp_already_exists.d66b"));
+
+
+        MachineFtpModel byKey = machineFtpServer.getByKey(id,false);
+        Optional.ofNullable(byKey).ifPresent(item -> {
+            model.setPassword(StrUtil.emptyToDefault(model.getPassword(), item.getPassword()));
+        });
+
+        // 测试连接
+        try (Ftp ftp = new Ftp(machineFtpServer.toFtpConfig(model),
+            EnumUtil.fromString(FtpMode.class, mode, FtpMode.Active)))  {
+            ftp.pwd();
+        } catch (Exception e) {
+            log.error(I18nMessageUtil.get("i18n.ftp_connection_failed.1f2f"), e);
+            return new JsonMessage<>(500, I18nMessageUtil.get("i18n.ftp_connection_failed_message.bd99") + e.getMessage());
+        }
+
+        model.setStatus(1);
+        int i = add ? machineFtpServer.insert(model) : machineFtpServer.updateById(model);
+        return JsonMessage.success(I18nMessageUtil.get("i18n.operation_succeeded.3313"));
+    }
+
+    @GetMapping(value = "list-workspace-ftp", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.LIST)
+    public IJsonMessage<List<FtpModel>> listWorkspaceFtp(@ValidatorItem String id) {
+        MachineFtpModel machineFtpModel = machineFtpServer.getByKey(id);
+        Assert.notNull(machineFtpModel, I18nMessageUtil.get("i18n.no_machine.89ed"));
+        FtpModel ftpModel = new FtpModel();
+        ftpModel.setMachineFtpId(id);
+        List<FtpModel> modelList = ftpService.listByBean(ftpModel);
+        modelList = Optional.ofNullable(modelList).orElseGet(ArrayList::new);
+        for (FtpModel model : modelList) {
+            model.setWorkspace(workspaceService.getByKey(model.getWorkspaceId()));
+        }
+        return JsonMessage.success("", modelList);
+    }
+
+    /**
+     * 将 ftp 分配到指定工作空间
+     *
+     * @param ids         ftp id
+     * @param workspaceId 工作空间id
+     * @return json
+     */
+    @PostMapping(value = "distribute", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.EDIT)
+    public IJsonMessage<String> distribute(@ValidatorItem String ids, @ValidatorItem String workspaceId) {
+        List<String> list = StrUtil.splitTrim(ids, StrUtil.COMMA);
+        for (String id : list) {
+            MachineFtpModel machineFtpModel = machineFtpServer.getByKey(id);
+            Assert.notNull(machineFtpModel, I18nMessageUtil.get("i18n.no_ftp_correspondence.23c4"));
+            boolean exists = workspaceService.exists(new WorkspaceModel(workspaceId));
+            Assert.state(exists, I18nMessageUtil.get("i18n.workspace_not_exist.a6fd"));
+            if (!ftpService.existsFtp2(workspaceId, id)) {
+                ftpService.insert(machineFtpModel, workspaceId);
+            }
+        }
+
+        return JsonMessage.success(I18nMessageUtil.get("i18n.operation_succeeded.3313"));
+    }
+
+
+    /**
+     * 保存工作空间配置
+     *
+     * @param fileDirs 文件夹
+     * @param id       ID
+     * @return json
+     */
+    @PostMapping(value = "save-workspace-config", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.EDIT)
+    public IJsonMessage<String> saveWorkspaceConfig(
+        String fileDirs,
+        @ValidatorItem String id,
+        String allowEditSuffix) {
+        FtpModel ftpModel = new FtpModel(id);
+        // 目录
+        if (StrUtil.isEmpty(fileDirs)) {
+            ftpModel.fileDirs(null);
+        } else {
+            List<String> list = StrSplitter.splitTrim(fileDirs, StrUtil.LF, true);
+            UserModel userModel = getUser();
+            Assert.state(!userModel.isDemoUser(), PermissionInterceptor.DEMO_TIP);
+            ftpModel.fileDirs(list);
+        }
+        // 获取允许编辑的后缀
+        List<String> allowEditSuffixList = AgentWhitelist.parseToList(allowEditSuffix, I18nMessageUtil.get("i18n.suffix_cannot_be_empty.ec72"));
+        ftpModel.allowEditSuffix(allowEditSuffixList);
+        ftpService.updateById(ftpModel);
+        return JsonMessage.success(I18nMessageUtil.get("i18n.operation_succeeded.3313"));
+    }
+
+    /**
+     * edit
+     *
+     * @param id ssh id
+     * @return json
+     */
+    @PostMapping(value = "rest-hide-field", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.EDIT)
+    public IJsonMessage<String> restHideField(@ValidatorItem String id) {
+        MachineFtpModel machineFtpModel = new MachineFtpModel();
+        machineFtpModel.setId(id);
+        machineFtpModel.setPassword(StrUtil.EMPTY);
+        machineFtpServer.updateById(machineFtpModel);
+        return new JsonMessage<>(200, I18nMessageUtil.get("i18n.operation_succeeded.3313"));
+    }
+
+    /**
+     * 下载导入模板
+     */
+    @GetMapping(value = "import-template", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.LIST)
+    public void importTemplate(HttpServletResponse response) throws IOException {
+        String prefix = I18nMessageUtil.get("i18n.ftp_import_template.8fa3");
+        String fileName = prefix + ".csv";
+        this.setApplicationHeader(response, fileName);
+        //
+        CsvWriter writer = CsvUtil.getWriter(response.getWriter());
+        writer.writeLine("name", "groupName", "host", "port", "user", "password", "serverLanguageCode", "systemKey", "charset", "mode", "timeout");
+        writer.flush();
+    }
+
+    /**
+     * 导出数据
+     */
+    @GetMapping(value = "export-data", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.DOWNLOAD)
+    public void exportData(HttpServletResponse response, HttpServletRequest request) throws IOException {
+        String prefix = I18nMessageUtil.get("i18n.exported_ftp_data.2b54");
+        String fileName = prefix + DateTime.now().toString(DatePattern.NORM_DATE_FORMAT) + ".csv";
+        this.setApplicationHeader(response, fileName);
+        //
+        CsvWriter writer = CsvUtil.getWriter(response.getWriter());
+        int pageInt = 0;
+        writer.writeLine("name", "groupName", "host", "port", "user", "password", "serverLanguageCode", "systemKey", "charset", "mode", "timeout");
+        while (true) {
+            Map<String, String> paramMap = ServletUtil.getParamMap(request);
+            paramMap.remove("workspaceId");
+            // 下一页
+            paramMap.put("page", String.valueOf(++pageInt));
+            PageResultDto<MachineFtpModel> listPage = machineFtpServer.listPage(paramMap, false);
+            if (listPage.isEmpty()) {
+                break;
+            }
+            listPage.getResult()
+                .stream()
+                .map((Function<MachineFtpModel, List<Object>>) machineSshModel -> CollUtil.newArrayList(
+                    machineSshModel.getName(),
+                    machineSshModel.getGroupName(),
+                    machineSshModel.getHost(),
+                    machineSshModel.getPort(),
+                    machineSshModel.getUser(),
+                    machineSshModel.getPassword(),
+                    machineSshModel.getCharset(),
+                    machineSshModel.getMode(),
+                    machineSshModel.getTimeout()
+                ))
+                .map(objects -> objects.stream().map(StrUtil::toStringOrNull).toArray(String[]::new))
+                .forEach(writer::writeLine);
+            if (ObjectUtil.equal(listPage.getPage(), listPage.getTotalPage())) {
+                // 最后一页
+                break;
+            }
+        }
+        writer.flush();
+    }
+
+    /**
+     * 导入数据
+     *
+     * @return json
+     */
+    @PostMapping(value = "import-data", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Feature(method = MethodFeature.UPLOAD)
+    public IJsonMessage<String> importData(MultipartFile file) throws IOException {
+        Assert.notNull(file, I18nMessageUtil.get("i18n.no_uploaded_file.07ef"));
+        String originalFilename = file.getOriginalFilename();
+        String extName = FileUtil.extName(originalFilename);
+        boolean csv = StrUtil.endWithIgnoreCase(extName, "csv");
+        Assert.state(csv, I18nMessageUtil.get("i18n.disallowed_file_format.d6e4"));
+        assert originalFilename != null;
+        File csvFile = FileUtil.file(serverConfig.getUserTempPath(), originalFilename);
+        int addCount = 0, updateCount = 0;
+        Charset fileCharset;
+        try {
+            file.transferTo(csvFile);
+            fileCharset = CharsetDetector.detect(csvFile);
+            Reader bomReader = FileUtil.getReader(csvFile, fileCharset);
+            CsvReadConfig csvReadConfig = CsvReadConfig.defaultConfig();
+            csvReadConfig.setHeaderLineNo(0);
+            CsvReader reader = CsvUtil.getReader(bomReader, csvReadConfig);
+            CsvData csvData;
+            try {
+                csvData = reader.read();
+            } catch (Exception e) {
+                log.error(I18nMessageUtil.get("i18n.parse_csv_exception.885e"), e);
+                return new JsonMessage<>(405, I18nMessageUtil.get("i18n.parse_file_exception.374d") + e.getMessage());
+            } finally {
+                IoUtil.close(reader);
+            }
+            List<CsvRow> rows = csvData.getRows();
+            Assert.notEmpty(rows, I18nMessageUtil.get("i18n.no_data.55a2"));
+
+            for (int i = 0; i < rows.size(); i++) {
+                CsvRow csvRow = rows.get(i);
+                String name = csvRow.getByName("name");
+                int finalI = i;
+                Assert.hasText(name, () -> StrUtil.format(I18nMessageUtil.get("i18n.name_field_required.e0c5"), finalI + 1));
+                String groupName = csvRow.getByName("groupName");
+                String host = csvRow.getByName("host");
+                Assert.hasText(host, () -> StrUtil.format(I18nMessageUtil.get("i18n.host_field_required.5c36"), finalI + 1));
+                Integer port = Convert.toInt(csvRow.getByName("port"));
+                Assert.state(port != null && NetUtil.isValidPort(port), () -> StrUtil.format(I18nMessageUtil.get("i18n.port_field_required_or_incorrect.8426"), finalI + 1));
+                String user = csvRow.getByName("user");
+                Assert.hasText(host, () -> StrUtil.format(I18nMessageUtil.get("i18n.user_field_required.8732"), finalI + 1));
+                String password = csvRow.getByName("password");
+                String charset = csvRow.getByName("charset");
+                //
+                String mode = csvRow.getByName("mode");
+                FtpMode ftpMode = EnumUtil.fromString(FtpMode.class, mode, FtpMode.Active);
+                mode = ftpMode.name();
+                String serverLanguageCode = csvRow.getByName("serverLanguageCode");
+                String systemKey = csvRow.getByName("systemKey");
+
+                Integer timeout = Convert.toInt(csvRow.getByName("timeout"));
+                //
+                MachineFtpModel where = new MachineFtpModel();
+                where.setHost(host);
+                where.setUser(user);
+                where.setPort(port);
+                where.setMode(mode);
+                MachineFtpModel machineFtpModel = machineFtpServer.queryByBean(where);
+                if (machineFtpModel == null) {
+                    // 添加
+                    where.setName(name);
+                    where.setGroupName(groupName);
+                    where.setPassword(password);
+                    where.setMode(mode);
+                    where.setTimeout(timeout);
+                    where.setCharset(charset);
+                    where.setServerLanguageCode(serverLanguageCode);
+                    where.setSystemKey(systemKey);
+                    machineFtpServer.insert(where);
+                    addCount++;
+                } else {
+                    MachineFtpModel update = new MachineFtpModel();
+                    update.setId(machineFtpModel.getId());
+                    update.setName(name);
+                    update.setGroupName(groupName);
+                    update.setPassword(password);
+                    update.setMode(mode);
+                    update.setTimeout(timeout);
+                    update.setCharset(charset);
+                    where.setServerLanguageCode(serverLanguageCode);
+                    where.setSystemKey(systemKey);
+                    machineFtpServer.updateById(update);
+                    updateCount++;
+                }
+            }
+        } finally {
+            FileUtil.del(csvFile);
+        }
+        String fileCharsetStr = Optional.ofNullable(fileCharset).map(Charset::name).orElse(StrUtil.EMPTY);
+        return JsonMessage.success(I18nMessageUtil.get("i18n.import_success_with_details.a4a0"), fileCharsetStr, addCount, updateCount);
+    }
+
+
+}

+ 75 - 0
modules/server/src/main/java/org/dromara/jpom/func/assets/controller/MachineFtpFileController.java

@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2019 Of Him Code Technology Studio
+ * Jpom is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ * 			http://license.coscl.org.cn/MulanPSL2
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+package org.dromara.jpom.func.assets.controller;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Opt;
+import cn.hutool.core.util.StrUtil;
+import java.util.List;
+import java.util.function.BiFunction;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.jpom.common.i18n.I18nMessageUtil;
+import org.dromara.jpom.func.assets.model.MachineFtpModel;
+import org.dromara.jpom.permission.ClassFeature;
+import org.dromara.jpom.permission.Feature;
+import org.dromara.jpom.permission.SystemPermission;
+import org.dromara.jpom.util.FileUtils;
+import org.dromara.jpom.util.StringUtil;
+import org.springframework.util.Assert;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author wxyShine
+ * @since 2025/05/28
+ */
+@RestController
+@RequestMapping(value = "/system/assets/ftp-file")
+@Feature(cls = ClassFeature.FTP_FILE)
+@Slf4j
+@SystemPermission
+public class MachineFtpFileController extends BaseFtpFileController {
+    @Override
+    protected <T> T checkConfigPath(String id, BiFunction<MachineFtpModel, ItemConfig, T> function) {
+        MachineFtpModel machineFtpModel = machineFtpServer.getByKey(id, false);
+        Assert.notNull(machineFtpModel, I18nMessageUtil.get("i18n.no_ftp_server_correspondence.1af7"));
+        return function.apply(machineFtpModel, new ItemConfig() {
+            @Override
+            public List<String> allowEditSuffix() {
+                return StringUtil.jsonConvertArray(machineFtpModel.getAllowEditSuffix(), String.class);
+            }
+
+            @Override
+            public List<String> fileDirs() {
+                return CollUtil.newArrayList(StrUtil.SLASH);
+            }
+        });
+    }
+
+    @Override
+    protected <T> T checkConfigPathChildren(String id, String path, String children, BiFunction<MachineFtpModel, ItemConfig, T> function) {
+        FileUtils.checkSlip(path);
+        Opt.ofBlankAble(children).ifPresent(FileUtils::checkSlip);
+        //
+        MachineFtpModel machineFtpModel = machineFtpServer.getByKey(id, false);
+        return function.apply(machineFtpModel, new ItemConfig() {
+            @Override
+            public List<String> allowEditSuffix() {
+                return StringUtil.jsonConvertArray(machineFtpModel.getAllowEditSuffix(), String.class);
+            }
+
+            @Override
+            public List<String> fileDirs() {
+                return CollUtil.newArrayList(StrUtil.SLASH);
+            }
+        });
+    }
+}

+ 1 - 1
modules/server/src/main/java/org/dromara/jpom/func/assets/controller/MachineSshController.java

@@ -155,7 +155,7 @@ public class MachineSshController extends BaseGroupNameController {
      * 编辑
      *
      * @param name        名称
-     * @param host        端口
+     * @param host        主机
      * @param user        用户名
      * @param password    密码
      * @param connectType 连接方式

+ 115 - 0
modules/server/src/main/java/org/dromara/jpom/func/assets/model/MachineFtpModel.java

@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2019 Of Him Code Technology Studio
+ * Jpom is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ * 			http://license.coscl.org.cn/MulanPSL2
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+package org.dromara.jpom.func.assets.model;
+
+import cn.hutool.core.util.CharsetUtil;
+import cn.hutool.extra.ftp.FtpConfig;
+import cn.hutool.extra.ftp.FtpMode;
+import com.alibaba.fastjson2.JSONArray;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import org.dromara.jpom.db.TableName;
+import org.dromara.jpom.model.BaseGroupNameModel;
+
+import java.util.List;
+
+/**
+ * @author bwcx_jzy
+ * @see FtpConfig
+ * @since 2024/08/31
+ */
+@EqualsAndHashCode(callSuper = true)
+@TableName(value = "MACHINE_FTP_INFO",
+    nameKey = "i18n.asset_ftp_info.3b75")
+@Data
+@NoArgsConstructor
+public class MachineFtpModel extends BaseGroupNameModel {
+
+    /**
+     * 主机地址
+     */
+    private String host;
+    /**
+     * 端口
+     */
+    private Integer port;
+    /**
+     * 登录账号
+     */
+    private String user;
+    /**
+     * 账号密码
+     */
+    private String password;
+    /**
+     * 编码格式
+     */
+    private String charset;
+    /**
+     * 超时时间
+     */
+    private Integer timeout;
+    /**
+     * 设置服务器语言
+     */
+    private String serverLanguageCode;
+
+    /**
+     * 设置服务器系统关键词
+     */
+    private String systemKey;
+    /**
+     * 模式
+     */
+    private String mode;
+    /**
+     * ftp连接状态
+     * <p>
+     * 状态{0,无法连接,1 正常,2 禁用监控}
+     */
+    private Integer status;
+    /**
+     * 状态消息
+     */
+    private String statusMsg;
+    /**
+     * 允许编辑后缀
+     */
+    private String allowEditSuffix;
+
+    public MachineFtpModel(String id) {
+        setId(id);
+    }
+
+    public void allowEditSuffix(List<String> allowEditSuffix) {
+        if (allowEditSuffix == null) {
+            this.allowEditSuffix = null;
+        } else {
+            this.allowEditSuffix = JSONArray.toJSONString(allowEditSuffix);
+        }
+    }
+
+    public FtpConfig toFtpConfig() {
+        FtpConfig ftpConfig = new FtpConfig();
+        ftpConfig.setHost(host);
+        ftpConfig.setPort(port);
+        ftpConfig.setUser(user);
+        ftpConfig.setPassword(password);
+        ftpConfig.setCharset(CharsetUtil.parse(charset));
+        if (timeout != null) {
+            ftpConfig.setSoTimeout(timeout * 1000);
+            ftpConfig.setConnectionTimeout(timeout * 1000);
+        }
+        ftpConfig.setSystemKey(systemKey);
+        ftpConfig.setServerLanguageCode(serverLanguageCode);
+        return ftpConfig;
+    }
+}

+ 245 - 0
modules/server/src/main/java/org/dromara/jpom/func/assets/server/MachineFtpServer.java

@@ -0,0 +1,245 @@
+package org.dromara.jpom.func.assets.server;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.lang.Opt;
+import cn.hutool.core.util.EnumUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.cron.task.Task;
+import cn.hutool.db.Entity;
+import cn.hutool.extra.ftp.Ftp;
+import cn.hutool.extra.ftp.FtpConfig;
+import cn.hutool.extra.ftp.FtpException;
+import cn.hutool.extra.ftp.FtpMode;
+import cn.keepbx.jpom.event.IAsyncLoad;
+import java.time.Duration;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import javax.annotation.PreDestroy;
+import lombok.Lombok;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.net.ftp.FTPClient;
+import org.dromara.jpom.common.Const;
+import org.dromara.jpom.common.ServerConst;
+import org.dromara.jpom.common.i18n.I18nMessageUtil;
+import org.dromara.jpom.configuration.AssetsConfig;
+import org.dromara.jpom.cron.CronUtils;
+import org.dromara.jpom.func.assets.AssetsExecutorPoolService;
+import org.dromara.jpom.func.assets.model.MachineFtpModel;
+import org.dromara.jpom.func.system.service.ClusterInfoService;
+import org.dromara.jpom.plugin.IWorkspaceEnvPlugin;
+import org.dromara.jpom.plugin.PluginFactory;
+import org.dromara.jpom.service.h2db.BaseDbService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * @author bwcx_jzy
+ * @since 2024/8/31
+ */
+@Service
+@Slf4j
+public class MachineFtpServer extends BaseDbService<MachineFtpModel> implements IAsyncLoad, Task {
+
+    private static final String CRON_ID = "ftp-monitor";
+
+    private final ClusterInfoService clusterInfoService;
+    private final AssetsConfig.FtpConfig ftpConfig;
+    private final AssetsExecutorPoolService assetsExecutorPoolService;
+
+    private final Map<String, Ftp> ftpConnectionPool = new ConcurrentHashMap<>();
+
+
+    public MachineFtpServer(ClusterInfoService clusterInfoService,
+                            AssetsConfig assetsConfig,
+                            AssetsExecutorPoolService assetsExecutorPoolService) {
+        this.clusterInfoService = clusterInfoService;
+        this.ftpConfig = assetsConfig.getFtp();
+        this.assetsExecutorPoolService = assetsExecutorPoolService;
+    }
+
+    /**
+     * 转换为配置对象
+     *
+     * @param model ftp model
+     * @return config
+     */
+    public FtpConfig toFtpConfig(MachineFtpModel model) {
+        String workspaceId = ServerConst.WORKSPACE_GLOBAL;
+        FtpConfig config = model.toFtpConfig();
+        String password = config.getPassword();
+        String user = config.getUser();
+        // 转化密码字段
+        IWorkspaceEnvPlugin plugin = (IWorkspaceEnvPlugin) PluginFactory.getPlugin(IWorkspaceEnvPlugin.PLUGIN_NAME);
+        try {
+            user = plugin.convertRefEnvValue(workspaceId, user);
+            password = plugin.convertRefEnvValue(workspaceId, password);
+        } catch (Exception e) {
+            throw Lombok.sneakyThrow(e);
+        }
+        config.setUser(user);
+        config.setPassword(password);
+        return config;
+    }
+
+    @Override
+    protected void fillInsert(MachineFtpModel model) {
+        super.fillInsert(model);
+        model.setGroupName(StrUtil.emptyToDefault(model.getGroupName(), Const.DEFAULT_GROUP_NAME.get()));
+        model.setStatus(ObjectUtil.defaultIfNull(model.getStatus(), 0));
+    }
+
+    @Override
+    protected void fillSelectResult(MachineFtpModel data) {
+        if (data == null) {
+            return;
+        }
+        if (!StrUtil.startWithIgnoreCase(data.getPassword(), ServerConst.REF_WORKSPACE_ENV)) {
+            // 隐藏密码字段
+            data.setPassword(null);
+        }
+    }
+
+    @Override
+    public void execute() {
+        Entity entity = new Entity();
+        if (clusterInfoService.isMultiServer()) {
+            String linkGroup = clusterInfoService.getCurrent().getLinkGroup();
+            List<String> linkGroups = StrUtil.splitTrim(linkGroup, StrUtil.COMMA);
+            if (CollUtil.isEmpty(linkGroups)) {
+                log.warn(I18nMessageUtil.get("i18n.cluster_not_grouped.8f54"));
+                return;
+            }
+            entity.set("groupName", linkGroups);
+        }
+        List<MachineFtpModel> list = this.listByEntity(entity, false);
+        if (CollUtil.isEmpty(list)) {
+            return;
+        }
+        this.checkList(list);
+    }
+
+
+    private void checkList(List<MachineFtpModel> monitorModels) {
+        monitorModels.forEach(monitorModel -> assetsExecutorPoolService.execute(() -> this.updateMonitor(monitorModel)));
+    }
+
+    /**
+     * 执行监控 ftp
+     *
+     * @param machineFtpModel 资产 ftp
+     */
+    private void updateMonitor(MachineFtpModel machineFtpModel) {
+        List<String> monitorGroupName = ftpConfig.getDisableMonitorGroupName();
+        if (CollUtil.containsAny(monitorGroupName, CollUtil.newArrayList(machineFtpModel.getGroupName(), "*"))) {
+            // 禁用监控
+            if (machineFtpModel.getStatus() != null && machineFtpModel.getStatus() == 2) {
+                // 不需要更新
+                return;
+            }
+            this.updateStatus(machineFtpModel.getId(), 2, I18nMessageUtil.get("i18n.disable_monitoring.4615"));
+            return;
+        }
+        log.warn("machineFtpModel:{}",machineFtpModel);
+        try (Ftp ftp = getFtpClient(machineFtpModel)) {
+            ftp.pwd();
+            //
+            this.updateStatus(machineFtpModel.getId(), 1, "");
+        } catch (Exception e) {
+            String message = e.getMessage();
+            String s = I18nMessageUtil.get("i18n.monitor_name.9aff");
+            if (StrUtil.containsIgnoreCase(message, "timeout")) {
+                String s1 = I18nMessageUtil.get("i18n.timeout.e944");
+                log.error("{} ftp[{}] {} {}", s, machineFtpModel.getName(), s1, message);
+            } else {
+                String s1 = I18nMessageUtil.get("i18n.exception.c195");
+                log.error("{} ftp[{}] {}", s, machineFtpModel.getName(), s1, e);
+            }
+            this.updateStatus(machineFtpModel.getId(), 0, message);
+        }
+    }
+
+    /**
+     * 更新 ftp状态
+     *
+     * @param id     ID
+     * @param status 状态值
+     * @param msg    错误消息
+     */
+    private void updateStatus(String id, int status, String msg) {
+        MachineFtpModel model = new MachineFtpModel();
+        model.setId(id);
+        model.setStatus(status);
+        model.setStatusMsg(msg);
+        super.updateById(model);
+    }
+
+    @Override
+    public void startLoad() {
+        String monitorCron = ftpConfig.getMonitorCron();
+        String cron = Opt.ofBlankAble(monitorCron).orElse("0 0/1 * * * ?");
+        CronUtils.add(CRON_ID, cron, () -> MachineFtpServer.this);
+    }
+
+
+    /**
+     * 获取或创建 Ftp 连接,自动重连失效连接
+     *
+     * @param machineFtpModel 模型
+     * @return Ftp 实例
+     */
+    public synchronized Ftp getFtpClient(MachineFtpModel machineFtpModel) {
+        MachineFtpModel model = this.getByKey(machineFtpModel.getId(), false);
+        Optional.ofNullable(model).ifPresent(machineSshModel -> {
+            machineFtpModel.setPassword(StrUtil.emptyToDefault(machineFtpModel.getPassword(), machineFtpModel.getPassword()));
+        });
+        String id = machineFtpModel.getId();
+        Ftp ftp = ftpConnectionPool.get(id);
+        try {
+            if (ftp != null) {
+                ftp.pwd();
+                return ftp;
+            }
+        } catch (Exception e) {
+            System.out.println(machineFtpModel);
+            log.warn(I18nMessageUtil.get("i18n.check_ftp_connection_failed.f7de"), id, e.getMessage());
+            IoUtil.close(ftp);
+        }
+
+        try {
+            FtpConfig config = toFtpConfig(machineFtpModel);
+            Ftp tmpFtp = new Ftp(config, EnumUtil.fromString(FtpMode.class, machineFtpModel.getMode(), FtpMode.Active));
+
+            tmpFtp.pwd();
+
+            ftpConnectionPool.put(id, tmpFtp);
+            return tmpFtp;
+        } catch (FtpException e) {
+            log.error(I18nMessageUtil.get("i18n.ftp_client_build_failure.aa55"), id, e.getMessage());
+            throw Lombok.sneakyThrow(e);
+        }
+//        return ftp;
+    }
+
+    /**
+     * 销毁所有连接(服务销毁时调用)
+     */
+/*    @PreDestroy
+    public void destroy() {
+        synchronized (ftpConnectionPool) {
+            for (Ftp ftp : ftpConnectionPool.values()) {
+                try {
+                    if (ftp != null && ftp.getClient().isAvailable()) {
+                        ftp.close();
+                    }
+                } catch (Exception e) {
+                    log.warn(I18nMessageUtil.get("i18n.ftp_connection_failure.0f31"), e);
+                }
+            }
+            ftpConnectionPool.clear();
+        }
+    }*/
+}

+ 1 - 1
modules/server/src/main/java/org/dromara/jpom/func/assets/server/MachineNodeStatLogServer.java

@@ -45,7 +45,7 @@ public class MachineNodeStatLogServer extends BaseDbService<MachineNodeStatLogMo
         Entity entity = Entity.create();
         DateTime dateTime = DateUtil.beginOfDay(DateTime.now());
         dateTime = DateUtil.offsetDay(dateTime, -statLogKeepDays);
-        entity.set(" monitorTime", "< " + dateTime.getTime());
+        entity.set("monitorTime", "< " + dateTime.getTime());
         int del = this.del(entity);
         log.info(I18nMessageUtil.get("i18n.auto_clear_machine_node_stats_logs.5279"), del);
     }

+ 6 - 2
modules/server/src/main/java/org/dromara/jpom/func/system/service/ClusterInfoService.java

@@ -28,6 +28,7 @@ import org.dromara.jpom.common.ServerConst;
 import org.dromara.jpom.common.i18n.I18nMessageUtil;
 import org.dromara.jpom.configuration.ClusterConfig;
 import org.dromara.jpom.cron.CronUtils;
+import org.dromara.jpom.dialect.DialectUtil;
 import org.dromara.jpom.func.assets.server.MachineDockerServer;
 import org.dromara.jpom.func.assets.server.MachineNodeServer;
 import org.dromara.jpom.func.assets.server.MachineSshServer;
@@ -198,8 +199,11 @@ public class ClusterInfoService extends BaseDbService<ClusterInfoModel> implemen
             return;
         }
         // 所以工作空间自动绑定集群Id
-        String sql = "update " + workspaceService.getTableName() + " set clusterInfoId=?";
-        workspaceService.execute(sql, installId);
+        Entity entity = new Entity();
+        entity.set(DialectUtil.wrapField("clusterInfoId"), installId);
+        workspaceService.update(entity, Entity.create().set(DialectUtil.wrapField("createTimeMillis"), "> 0"));
+        //String sql = "update " + workspaceService.getTableName() + " set clusterInfoId=?";
+        //workspaceService.execute(sql, installId);
         // 获取所有的资产分组
         List<String> list = this.listLinkGroups();
         String join = CollUtil.join(list, StrUtil.COMMA);

+ 125 - 0
modules/server/src/main/java/org/dromara/jpom/model/data/FtpModel.java

@@ -0,0 +1,125 @@
+/*
+ * Copyright (c) 2019 Of Him Code Technology Studio
+ * Jpom is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ * 			http://license.coscl.org.cn/MulanPSL2
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+package org.dromara.jpom.model.data;
+
+import cn.hutool.core.annotation.PropIgnore;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.fastjson2.JSONArray;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import org.dromara.jpom.db.TableName;
+import org.dromara.jpom.func.assets.controller.BaseFtpFileController;
+import org.dromara.jpom.func.assets.model.MachineFtpModel;
+import org.dromara.jpom.model.BaseGroupModel;
+import org.dromara.jpom.util.StringUtil;
+
+
+/**
+ * ftp信息
+ *
+ * @author wxyShine
+ * @since 2025/05/29
+ */
+@TableName(value = "FTP_INFO",
+    nameKey = "i18n.ftp_info_table.b177")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@NoArgsConstructor
+public class FtpModel extends BaseGroupModel implements BaseFtpFileController.ItemConfig {
+
+    private String name;
+    @Deprecated
+    private String host;
+    @Deprecated
+    private Integer port;
+    @Deprecated
+    private String user;
+    @Deprecated
+    private String password;
+    /**
+     * 编码格式
+     */
+    @Deprecated
+    private String charset;
+
+    /**
+     * 文件目录
+     */
+    private String fileDirs;
+
+    /**
+     * 允许编辑的后缀文件
+     */
+    private String allowEditSuffix;
+    /**
+     * 节点超时时间
+     */
+    @Deprecated
+    private Integer timeout;
+
+    /**
+     * ftp id
+     */
+    private String machineFtpId;
+
+    @PropIgnore
+    private MachineFtpModel machineFtp;
+
+
+    @PropIgnore
+    private WorkspaceModel workspace;
+
+    public FtpModel(String id) {
+        this.setId(id);
+    }
+
+
+    @Override
+    public List<String> fileDirs() {
+        List<String> strings = StringUtil.jsonConvertArray(this.fileDirs, String.class);
+        return Optional.ofNullable(strings)
+            .map(strings1 -> strings1.stream()
+                .map(s -> FileUtil.normalize(StrUtil.SLASH + s + StrUtil.SLASH))
+                .collect(Collectors.toList()))
+            .orElse(null);
+    }
+
+    public void fileDirs(List<String> fileDirs) {
+        if (fileDirs != null) {
+            for (int i = fileDirs.size() - 1; i >= 0; i--) {
+                String s = fileDirs.get(i);
+                fileDirs.set(i, FileUtil.normalize(s));
+            }
+            this.fileDirs = JSONArray.toJSONString(fileDirs);
+        } else {
+            this.fileDirs = StrUtil.EMPTY;
+        }
+    }
+
+
+    @Override
+    public List<String> allowEditSuffix() {
+        return StringUtil.jsonConvertArray(this.allowEditSuffix, String.class);
+    }
+
+    public void allowEditSuffix(List<String> allowEditSuffix) {
+        if (allowEditSuffix == null) {
+            this.allowEditSuffix = null;
+        } else {
+            this.allowEditSuffix = JSONArray.toJSONString(allowEditSuffix);
+        }
+    }
+}

+ 1 - 0
modules/server/src/main/java/org/dromara/jpom/model/enums/BuildReleaseMethod.java

@@ -25,6 +25,7 @@ public enum BuildReleaseMethod implements BaseEnum {
 	Ssh(3, "SSH"),
 	LocalCommand(4, "本地命令行"),
 	DockerImage(5, "Docker镜像"),
+	Ftp(6, "FTP"),
 	;
 	private final int code;
 	private final String desc;

+ 15 - 3
modules/server/src/main/java/org/dromara/jpom/permission/ClassFeature.java

@@ -9,9 +9,11 @@
  */
 package org.dromara.jpom.permission;
 
+import java.util.function.Supplier;
 import lombok.Getter;
 import org.dromara.jpom.common.i18n.I18nMessageUtil;
 import org.dromara.jpom.func.assets.server.MachineDockerServer;
+import org.dromara.jpom.func.assets.server.MachineFtpServer;
 import org.dromara.jpom.func.assets.server.MachineNodeServer;
 import org.dromara.jpom.func.assets.server.MachineSshServer;
 import org.dromara.jpom.func.assets.server.ScriptLibraryServer;
@@ -21,7 +23,13 @@ import org.dromara.jpom.func.files.service.FileStorageService;
 import org.dromara.jpom.func.files.service.StaticFileStorageService;
 import org.dromara.jpom.func.system.service.ClusterInfoService;
 import org.dromara.jpom.func.user.server.UserLoginLogServer;
-import org.dromara.jpom.service.dblog.*;
+import org.dromara.jpom.service.dblog.BackupInfoService;
+import org.dromara.jpom.service.dblog.BuildInfoService;
+import org.dromara.jpom.service.dblog.DbBuildHistoryLogService;
+import org.dromara.jpom.service.dblog.DbMonitorNotifyLogService;
+import org.dromara.jpom.service.dblog.DbUserOperateLogService;
+import org.dromara.jpom.service.dblog.RepositoryService;
+import org.dromara.jpom.service.dblog.SshTerminalExecuteLogService;
 import org.dromara.jpom.service.docker.DockerInfoService;
 import org.dromara.jpom.service.docker.DockerSwarmInfoService;
 import org.dromara.jpom.service.h2db.BaseDbService;
@@ -29,6 +37,7 @@ import org.dromara.jpom.service.monitor.MonitorService;
 import org.dromara.jpom.service.monitor.MonitorUserOptService;
 import org.dromara.jpom.service.node.NodeService;
 import org.dromara.jpom.service.node.ProjectInfoCacheService;
+import org.dromara.jpom.service.node.ftp.FtpService;
 import org.dromara.jpom.service.node.script.NodeScriptExecuteLogServer;
 import org.dromara.jpom.service.node.script.NodeScriptServer;
 import org.dromara.jpom.service.node.ssh.CommandExecLogService;
@@ -44,8 +53,6 @@ import org.dromara.jpom.service.system.WorkspaceService;
 import org.dromara.jpom.service.user.UserPermissionGroupServer;
 import org.dromara.jpom.service.user.UserService;
 
-import java.util.function.Supplier;
-
 /**
  * 功能模块
  *
@@ -68,6 +75,8 @@ public enum ClassFeature {
     SSH_TERMINAL_LOG(() -> I18nMessageUtil.get("i18n.ssh_terminal_log.775f"), SshTerminalExecuteLogService.class),
     SSH_COMMAND(() -> I18nMessageUtil.get("i18n.ssh_command_management.c40a"), SshCommandService.class),
     SSH_COMMAND_LOG(() -> I18nMessageUtil.get("i18n.ssh_command_log.7fd1"), CommandExecLogService.class),
+    FTP(() -> I18nMessageUtil.get("i18n.ftp_management.cb91"), FtpService.class),
+    FTP_FILE(() -> I18nMessageUtil.get("i18n.ftp_file_manager.c52e"), FtpService.class),
     OUTGIVING(() -> I18nMessageUtil.get("i18n.distribute_management.3a2d"), OutGivingServer.class),
     LOG_READ(() -> I18nMessageUtil.get("i18n.log_reading.a4c8"), LogReadServer.class),
     OUTGIVING_LOG(() -> I18nMessageUtil.get("i18n.distribute_log.c612"), DbOutGivingLogService.class),
@@ -77,6 +86,8 @@ public enum ClassFeature {
     OPT_MONITOR(() -> I18nMessageUtil.get("i18n.operation_monitoring.0cd5"), MonitorUserOptService.class),
     DOCKER(() -> I18nMessageUtil.get("i18n.docker_management.e7e5"), DockerInfoService.class),
     DOCKER_SWARM(() -> I18nMessageUtil.get("i18n.container_cluster.a5b4"), DockerSwarmInfoService.class),
+
+
     /**
      * ssh
      */
@@ -98,6 +109,7 @@ public enum ClassFeature {
     SYSTEM_UPGRADE(() -> I18nMessageUtil.get("i18n.online_upgrade.da8c")),
     SYSTEM_ASSETS_MACHINE(() -> I18nMessageUtil.get("i18n.machine_asset_management.36ea"), MachineNodeServer.class),
     SYSTEM_ASSETS_MACHINE_SSH(() -> I18nMessageUtil.get("i18n.ssh_asset_management.3b6c"), MachineSshServer.class),
+    SYSTEM_ASSETS_MACHINE_FTP(() -> I18nMessageUtil.get("i18n.ftp_asset_management.c6a5"), MachineFtpServer.class),
     SYSTEM_ASSETS_MACHINE_DOCKER(() -> I18nMessageUtil.get("i18n.docker_asset_management.96d9"), MachineDockerServer.class),
     SYSTEM_ASSETS_GLOBAL_SCRIPT(() -> I18nMessageUtil.get("i18n.script_library.aed1"), ScriptLibraryServer.class),
     SYSTEM_CONFIG(() -> I18nMessageUtil.get("i18n.server_system_config.3181")),

+ 3 - 3
modules/server/src/main/java/org/dromara/jpom/service/dblog/BackupInfoService.java

@@ -175,11 +175,11 @@ public class BackupInfoService extends BaseDbService<BackupInfoModel> implements
         final String fileName = LocalDateTimeUtil.format(LocalDateTimeUtil.now(), DatePattern.PURE_DATETIME_PATTERN);
 
         // 设置默认备份 SQL 的文件地址
-        File file = FileUtil.file(StorageServiceFactory.dbLocalPath(), DbExtConfig.BACKUP_DIRECTORY_NAME, fileName + DbExtConfig.SQL_FILE_SUFFIX);
+        File file = FileUtil.file(StorageServiceFactory.getInstance().dbLocalPath(), DbExtConfig.BACKUP_DIRECTORY_NAME, fileName + DbExtConfig.SQL_FILE_SUFFIX);
         final String backupSqlPath = FileUtil.getAbsolutePath(file);
 
         // 数据源参数
-        final String url = StorageServiceFactory.get().dbUrl();
+        final String url = StorageServiceFactory.getInstance().get().dbUrl();
 
         final String user = dbExtConfig.userName();
         final String pass = dbExtConfig.userPwd();
@@ -205,7 +205,7 @@ public class BackupInfoService extends BaseDbService<BackupInfoModel> implements
             backupInfo.setId(backupInfoModel.getId());
             try {
                 log.debug(I18nMessageUtil.get("i18n.start_new_thread_for_h2_database_backup.9337"));
-                StorageServiceFactory.get().backupSql(url, user, pass, backupSqlPath, tableNameList);
+                StorageServiceFactory.getInstance().get().backupSql(url, user, pass, backupSqlPath, tableNameList);
                 // 修改备份任务执行完成
                 backupInfo.setFileSize(FileUtil.size(file));
                 backupInfo.setSha1Sum(SecureUtil.sha1(file));

+ 33 - 5
modules/server/src/main/java/org/dromara/jpom/service/dblog/BuildInfoService.java

@@ -9,15 +9,18 @@
  */
 package org.dromara.jpom.service.dblog;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.cron.task.Task;
 import cn.hutool.db.Entity;
 import cn.hutool.extra.spring.SpringUtil;
 import cn.keepbx.jpom.cron.ICron;
+import java.util.stream.Collectors;
 import lombok.extern.slf4j.Slf4j;
 import org.dromara.jpom.build.BuildExecuteService;
 import org.dromara.jpom.common.BaseServerController;
 import org.dromara.jpom.cron.CronUtils;
+import org.dromara.jpom.dialect.DialectUtil;
 import org.dromara.jpom.model.data.BuildInfoModel;
 import org.dromara.jpom.model.enums.BuildReleaseMethod;
 import org.dromara.jpom.model.enums.BuildStatus;
@@ -107,15 +110,36 @@ public class BuildInfoService extends BaseWorkspaceService<BuildInfoModel> imple
      */
     @Override
     public List<BuildInfoModel> queryStartingList() {
-        String sql = "select * from " + super.getTableName() + " where autoBuildCron is not null and autoBuildCron <> ''";
+        String autoBuildCron = DialectUtil.wrapField("autoBuildCron");
+        String sql =StrUtil.format("select * from {} where {} is not null and {} <> ''",
+        super.getTableName(),autoBuildCron,autoBuildCron);
         return super.queryList(sql);
     }
 
-    @Override
+/*    @Override
     public int statusRecover() {
         // 恢复异常数据
         String updateSql = "update " + super.getTableName() + " set status=? where status=? or status=? or status=?";
         return super.execute(updateSql, BuildStatus.AbnormalShutdown.getCode(), BuildStatus.Ing.getCode(), BuildStatus.PubIng.getCode(), BuildStatus.WaitExec.getCode());
+    }*/
+
+    @Override
+    public int statusRecover() {
+        // 创建 Entity 对象并设置更新的字段和新值
+        Entity entity = Entity.create(super.getTableName());
+        entity.set("status", BuildStatus.AbnormalShutdown.getCode()); // 设置新状态为 AbnormalShutdown
+
+        // 创建条件 Entity 对象,模拟多个条件
+        Entity condition = Entity.create(super.getTableName())
+            // 状态为 Ing  或者状态为 PubIng 或者状态为 WaitExec
+            .set("status", CollUtil.newArrayList(
+                BuildStatus.Ing.getCode(),
+                BuildStatus.PubIng.getCode(),
+                BuildStatus.WaitExec.getCode())
+            );
+
+        return this.update(entity,condition);
+//        return super.execute(entity, condition);
     }
 
     /**
@@ -145,9 +169,13 @@ public class BuildInfoService extends BaseWorkspaceService<BuildInfoModel> imple
 
 
     public List<BuildInfoModel> hasResultKeep() {
-        //
-        String sql = "select * from " + super.getTableName() + " where resultKeepDay>0";
-        return super.queryList(sql);
+        Entity entity = Entity.create("BUILD_INFO");
+        entity.set("resultKeepDay", "> 0");
+        List<Entity> list = super.queryList(entity);
+
+        return list.stream()
+            .map(e -> e.toBean(BuildInfoModel.class))
+            .collect(Collectors.toList());
     }
 
     private static class CronTask implements Task {

+ 2 - 1
modules/server/src/main/java/org/dromara/jpom/service/docker/DockerInfoService.java

@@ -15,6 +15,7 @@ import cn.hutool.db.Entity;
 import cn.hutool.db.sql.Condition;
 import lombok.extern.slf4j.Slf4j;
 import org.dromara.jpom.common.i18n.I18nMessageUtil;
+import org.dromara.jpom.dialect.DialectUtil;
 import org.dromara.jpom.model.docker.DockerInfoModel;
 import org.dromara.jpom.service.h2db.BaseWorkspaceService;
 import org.springframework.stereotype.Service;
@@ -77,7 +78,7 @@ public class DockerInfoService extends BaseWorkspaceService<DockerInfoModel> {
      * @return count
      */
     public int countByTag(String workspaceId, String tag) {
-        String sql = StrUtil.format("SELECT * FROM {} where workspaceId=? and instr(tags,?)", super.getTableName());
+        String sql = StrUtil.format("SELECT * FROM {} where {}=? and instr(tags,?)", super.getTableName(), DialectUtil.wrapField("workspaceId"));
         return (int) super.count(sql, workspaceId, StrUtil.wrap(tag, StrUtil.COLON));
     }
 

+ 3 - 4
modules/server/src/main/java/org/dromara/jpom/service/h2db/BaseDbService.java

@@ -54,9 +54,6 @@ import java.util.stream.Stream;
 @Slf4j
 public abstract class BaseDbService<T extends BaseDbModel> extends BaseDbCommonService<T> {
 
-    @Autowired
-    @Lazy
-    private DbExtConfig extConfig;
     /**
      * 旧版本分组
      */
@@ -102,7 +99,9 @@ public abstract class BaseDbService<T extends BaseDbModel> extends BaseDbCommonS
      * @return list
      */
     public List<String> listGroupName() {
-        String sql = "select groupName from " + this.getTableName() + " group by groupName";
+        String group = DialectUtil.wrapField("groupName");
+        String sql = String.format("select %s from %s group by %s", group, getTableName(), group);
+        //String sql = "select groupName from " + this.getTableName() + " group by groupName";
         return this.listGroupByName(sql, "groupName");
     }
 

+ 255 - 0
modules/server/src/main/java/org/dromara/jpom/service/node/ftp/FtpService.java

@@ -0,0 +1,255 @@
+/*
+ * Copyright (c) 2019 Of Him Code Technology Studio
+ * Jpom is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ * 			http://license.coscl.org.cn/MulanPSL2
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+package org.dromara.jpom.service.node.ftp;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.NumberUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.ftp.Ftp;
+import com.jcraft.jsch.JSch;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
+import javax.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.jpom.common.ServerConst;
+import org.dromara.jpom.common.i18n.I18nMessageUtil;
+import org.dromara.jpom.configuration.BuildExtConfig;
+import org.dromara.jpom.func.assets.model.MachineFtpModel;
+import org.dromara.jpom.func.assets.server.MachineFtpServer;
+import org.dromara.jpom.model.data.FtpModel;
+import org.dromara.jpom.plugins.JschLogger;
+import org.dromara.jpom.service.h2db.BaseWorkspaceService;
+import org.dromara.jpom.util.LogRecorder;
+import org.dromara.jpom.util.MySftp;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.util.Assert;
+
+
+/**
+ * @author wxyShine
+ * @since 2025/05/29
+ */
+@Service
+@Slf4j
+public class FtpService extends BaseWorkspaceService<FtpModel> {
+
+    @Resource
+    @Lazy
+    private MachineFtpServer machineFtpServer;
+    private final BuildExtConfig buildExtConfig;
+
+    public FtpService(BuildExtConfig buildExtConfig) {
+        this.buildExtConfig = buildExtConfig;
+        JSch.setLogger(JschLogger.LOGGER);
+    }
+
+    @Override
+    protected void fillSelectResult(FtpModel data) {
+        if (data == null) {
+            return;
+        }
+        if (!StrUtil.startWithIgnoreCase(data.getPassword(), ServerConst.REF_WORKSPACE_ENV)) {
+            // 隐藏密码字段
+            data.setPassword(null);
+        }
+    }
+
+    @Override
+    protected void fillInsert(FtpModel ftpModel) {
+        super.fillInsert(ftpModel);
+        ftpModel.setHost(StrUtil.EMPTY);
+        ftpModel.setUser(StrUtil.EMPTY);
+        ftpModel.setPort(0);
+    }
+
+
+    /**
+     * 获取 ftp 配置对象
+     *
+     * @param ftpModel ftpModel
+     * @return session
+     */
+    public MachineFtpModel getMachineFtpModel(FtpModel ftpModel) {
+        MachineFtpModel ftpModel1 = machineFtpServer.getByKey(ftpModel.getMachineFtpId(), false);
+        Assert.notNull(ftpModel1, I18nMessageUtil.get("i18n.no_matching_asset_ftp.d420"));
+        return ftpModel1;
+    }
+
+
+    /**
+     * 将ssh信息同步到其他工作空间
+     *
+     * @param ids            多给节点ID
+     * @param nowWorkspaceId 当前的工作空间ID
+     * @param workspaceId    同步到哪个工作空间
+     */
+    public void syncToWorkspace(String ids, String nowWorkspaceId, String workspaceId) {
+        StrUtil.splitTrim(ids, StrUtil.COMMA)
+            .forEach(id -> {
+                FtpModel data = super.getByKey(id, false, entity -> entity.set("workspaceId", nowWorkspaceId));
+                Assert.notNull(data, I18nMessageUtil.get("i18n.no_corresponding_ssh_info.d864"));
+                //
+                FtpModel where = new FtpModel();
+                where.setWorkspaceId(workspaceId);
+                where.setMachineFtp(data.getMachineFtp());
+                FtpModel ftpModel = super.queryByBean(where);
+                Assert.isNull(ftpModel, I18nMessageUtil.get("i18n.workspace_ssh_already_exists.ccc0"));
+                // 不存在则添加 信息
+                data.setId(null);
+                data.setWorkspaceId(workspaceId);
+                data.setCreateTimeMillis(null);
+                data.setModifyTimeMillis(null);
+                data.setModifyUser(null);
+                data.setHost(null);
+                data.setUser(null);
+                data.setPassword(null);
+                data.setPort(null);
+                data.setCharset(null);
+                data.setTimeout(null);
+                super.insert(data);
+            });
+    }
+
+
+    public boolean existsFtp2(String workspaceId, String machineFtpId) {
+        //
+        FtpModel where = new FtpModel();
+        where.setWorkspaceId(workspaceId);
+        where.setMachineFtpId(machineFtpId);
+        return this.exists(where);
+    }
+
+    public void insert(MachineFtpModel machineFtpModel, String workspaceId) {
+        FtpModel data = new FtpModel();
+        data.setWorkspaceId(workspaceId);
+        data.setName(machineFtpModel.getName());
+        data.setGroup(machineFtpModel.getGroupName());
+        data.setMachineFtpId(machineFtpModel.getId());
+        data.setMachineFtpId(machineFtpModel.getId());
+        this.insert(data);
+    }
+
+    /**
+     * 创建文件上传 进度
+     *
+     * @param logRecorder 日志记录器
+     * @return 进度监听
+     */
+    public MySftp.ProgressMonitor createProgressMonitor(LogRecorder logRecorder) {
+        Set<Integer> progressRangeList = ConcurrentHashMap.newKeySet((int) Math.floor((float) 100 / buildExtConfig.getLogReduceProgressRatio()));
+        return new MySftp.ProgressMonitor() {
+            @Override
+            public void rest() {
+                progressRangeList.clear();
+            }
+
+            @Override
+            public void progress(String desc, long max, long now) {
+                double progressPercentage = Math.floor(((float) now / max) * 100);
+                int progressRange = (int) Math.floor(progressPercentage / buildExtConfig.getLogReduceProgressRatio());
+                if (progressRangeList.add(progressRange)) {
+                    //  total, progressSize
+                    logRecorder.system(I18nMessageUtil.get("i18n.upload_progress_with_units.44ad"), desc,
+                        FileUtil.readableFileSize(now), FileUtil.readableFileSize(max),
+                        NumberUtil.formatPercent(((float) now / max), 0)
+                    );
+                }
+            }
+        };
+    }
+
+    public FtpModel getByMachineFtpId(String id) {
+        FtpModel model = new FtpModel();
+        model.setMachineFtpId(id);
+        return queryByBean(model);
+    }
+
+    /**
+     * FTP 上传文件(带进度)
+     *
+     * @param ftp              hutool 的 ftp 对象
+     * @param localFile        本地文件
+     * @param remotePath       远程文件完整路径
+     * @param logRecorder      日志记录器
+     * @param progressRatio    进度输出频率,例如 5 表示每 5% 输出一次
+     */
+    public void uploadWithProgress(Ftp ftp, File localFile, String remotePath,
+                                          LogRecorder logRecorder, int progressRatio) throws IOException {
+        long total = localFile.length();
+        Set<Integer> progressRangeList = ConcurrentHashMap.newKeySet((int) (100f / progressRatio));
+
+        try (InputStream in = Files.newInputStream(localFile.toPath())) {
+            ProgressInputStream progressInputStream = new ProgressInputStream(in, total, now -> {
+                double percent = Math.floor((double) now / total * 100);
+                int range = (int) (percent / progressRatio);
+                if (progressRangeList.add(range)) {
+                    logRecorder.system(I18nMessageUtil.get("i18n.upload_progress_with_units.44ad"),
+                        localFile.getName(),
+                        FileUtil.readableFileSize(now),
+                        FileUtil.readableFileSize(total),
+                        String.format("%.0f%%", percent)
+                    );
+                }
+            });
+
+             remotePath = remotePath + StrUtil.SLASH + localFile.getName();
+            ftp.getClient().storeFile(remotePath, progressInputStream);
+        }
+    }
+
+    /**
+     * 包装带进度的 InputStream
+     */
+    private static class ProgressInputStream extends InputStream {
+        private final InputStream in;
+        private final long totalSize;
+        private long readSize = 0;
+        private final Consumer<Long> onProgress;
+
+        public ProgressInputStream(InputStream in, long totalSize, Consumer<Long> onProgress) {
+            this.in = in;
+            this.totalSize = totalSize;
+            this.onProgress = onProgress;
+        }
+
+        @Override
+        public int read() throws IOException {
+            int b = in.read();
+            if (b != -1) {
+                readSize++;
+                onProgress.accept(readSize);
+            }
+            return b;
+        }
+
+        @Override
+        public int read(byte[] b, int off, int len) throws IOException {
+            int count = in.read(b, off, len);
+            if (count > 0) {
+                readSize += count;
+                onProgress.accept(readSize);
+            }
+            return count;
+        }
+
+        @Override
+        public void close() throws IOException {
+            in.close();
+        }
+    }
+}

+ 5 - 1
modules/server/src/main/java/org/dromara/jpom/service/node/script/NodeScriptServer.java

@@ -9,12 +9,14 @@
  */
 package org.dromara.jpom.service.node.script;
 
+import cn.hutool.core.util.StrUtil;
 import cn.hutool.db.Entity;
 import com.alibaba.fastjson2.JSONArray;
 import com.alibaba.fastjson2.JSONObject;
 import org.dromara.jpom.common.forward.NodeForward;
 import org.dromara.jpom.common.forward.NodeUrl;
 import org.dromara.jpom.common.i18n.I18nMessageUtil;
+import org.dromara.jpom.dialect.DialectUtil;
 import org.dromara.jpom.func.assets.model.MachineNodeModel;
 import org.dromara.jpom.model.data.NodeModel;
 import org.dromara.jpom.model.node.NodeScriptCacheModel;
@@ -46,7 +48,9 @@ public class NodeScriptServer extends BaseNodeService<NodeScriptCacheModel> impl
      * @return nodeId list
      */
     public List<String> hasScriptNode() {
-        String sql = "select nodeId from " + super.getTableName() + " group by nodeId ";
+        String nodeId = DialectUtil.wrapField("nodeId");
+        String sql = StrUtil.format("select {} from {} group by {} ",
+            nodeId,super.getTableName(),nodeId);
         List<Entity> query = super.query(sql);
         if (query == null) {
             return null;

+ 4 - 1
modules/server/src/main/java/org/dromara/jpom/service/node/ssh/SshCommandService.java

@@ -22,6 +22,7 @@ import org.dromara.jpom.common.BaseServerController;
 import org.dromara.jpom.common.i18n.I18nMessageUtil;
 import org.dromara.jpom.common.i18n.I18nThreadUtil;
 import org.dromara.jpom.cron.CronUtils;
+import org.dromara.jpom.dialect.DialectUtil;
 import org.dromara.jpom.func.assets.model.MachineSshModel;
 import org.dromara.jpom.func.assets.server.ScriptLibraryServer;
 import org.dromara.jpom.model.EnvironmentMapBuilder;
@@ -125,7 +126,9 @@ public class SshCommandService extends BaseWorkspaceService<CommandModel> implem
      */
     @Override
     public List<CommandModel> queryStartingList() {
-        String sql = "select * from " + super.getTableName() + " where autoExecCron is not null and autoExecCron <> ''";
+        String autoExecCron = DialectUtil.wrapField("autoExecCron");
+        String sql =StrUtil.format("select * from {} where {} is not null and {} <> ''",
+            super.getTableName(),autoExecCron,autoExecCron);
         return super.queryList(sql);
     }
 

+ 8 - 2
modules/server/src/main/java/org/dromara/jpom/service/outgiving/OutGivingServer.java

@@ -9,6 +9,7 @@
  */
 package org.dromara.jpom.service.outgiving;
 
+import cn.hutool.db.Entity;
 import org.dromara.jpom.model.outgiving.OutGivingModel;
 import org.dromara.jpom.model.outgiving.OutGivingNodeProject;
 import org.dromara.jpom.service.IStatusRecover;
@@ -59,7 +60,12 @@ public class OutGivingServer extends BaseWorkspaceService<OutGivingModel> implem
     @Override
     public int statusRecover() {
         // 恢复异常数据
-        String updateSql = "update " + super.getTableName() + " set status=? where status=?";
-        return super.execute(updateSql, OutGivingModel.Status.DONE.getCode(), OutGivingModel.Status.ING.getCode());
+        Entity entity = Entity.create(super.getTableName());
+        String statusField = "status";
+        entity.set(statusField, OutGivingModel.Status.DONE.getCode());
+
+        Entity condition = Entity.create(super.getTableName());
+        condition.set(statusField, OutGivingModel.Status.ING.getCode());
+        return super.update(entity, condition);
     }
 }

+ 6 - 1
modules/server/src/main/java/org/dromara/jpom/service/script/ScriptServer.java

@@ -9,14 +9,17 @@
  */
 package org.dromara.jpom.service.script;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.cron.task.Task;
+import cn.hutool.db.Entity;
 import cn.hutool.extra.spring.SpringUtil;
 import cn.keepbx.jpom.cron.ICron;
 import lombok.extern.slf4j.Slf4j;
 import org.dromara.jpom.common.BaseServerController;
 import org.dromara.jpom.common.i18n.I18nMessageUtil;
 import org.dromara.jpom.cron.CronUtils;
+import org.dromara.jpom.dialect.DialectUtil;
 import org.dromara.jpom.model.script.ScriptExecuteLogModel;
 import org.dromara.jpom.model.script.ScriptModel;
 import org.dromara.jpom.model.user.UserModel;
@@ -40,7 +43,9 @@ public class ScriptServer extends BaseGlobalOrWorkspaceService<ScriptModel> impl
 
     @Override
     public List<ScriptModel> queryStartingList() {
-        String sql = "select * from " + super.getTableName() + " where autoExecCron is not null and autoExecCron <> ''";
+        String autoExecCron = DialectUtil.wrapField("autoExecCron");
+        String sql =StrUtil.format("select * from {} where {} is not null and {} <> ''",
+            super.getTableName(),autoExecCron,autoExecCron);
         return super.queryList(sql);
     }
 

+ 10 - 2
modules/server/src/main/java/org/dromara/jpom/service/system/WorkspaceService.java

@@ -9,10 +9,14 @@
  */
 package org.dromara.jpom.service.system;
 
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.db.Entity;
+import java.util.Arrays;
 import lombok.extern.slf4j.Slf4j;
 import org.dromara.jpom.common.Const;
 import org.dromara.jpom.common.i18n.I18nMessageUtil;
 import org.dromara.jpom.db.TableName;
+import org.dromara.jpom.dialect.DialectUtil;
 import org.dromara.jpom.model.BaseWorkspaceModel;
 import org.dromara.jpom.model.data.WorkspaceModel;
 import org.dromara.jpom.service.IStatusRecover;
@@ -48,10 +52,14 @@ public class WorkspaceService extends BaseDbService<WorkspaceModel> implements I
             if (tableName == null) {
                 continue;
             }
-            String sql = "update " + tableName.value() + " set workspaceId=? where (workspaceId is null or workspaceId='' or workspaceId='null')";
+
+            String workspaceId = DialectUtil.wrapField("workspaceId");
+            String value = super.parseRealTableName(tableName);
+            String sql = StrUtil.format("update " + value + " set {}=? where ({} is null or {}='' or {}='null')",
+                workspaceId,workspaceId,workspaceId,workspaceId);
             int execute = this.execute(sql, Const.WORKSPACE_DEFAULT_ID);
             if (execute > 0) {
-                log.info(I18nMessageUtil.get("i18n.fix_null_workspace_data.4d0b"), tableName.value(), execute);
+                log.info(I18nMessageUtil.get("i18n.fix_null_workspace_data.4d0b"), value, execute);
             }
             total += execute;
         }

+ 3 - 2
modules/server/src/main/java/org/dromara/jpom/service/user/TriggerTokenLogServer.java

@@ -21,6 +21,7 @@ import cn.keepbx.jpom.event.ISystemTask;
 import com.alibaba.fastjson2.JSONObject;
 import lombok.extern.slf4j.Slf4j;
 import org.dromara.jpom.common.i18n.I18nMessageUtil;
+import org.dromara.jpom.dialect.DialectUtil;
 import org.dromara.jpom.model.PageResultDto;
 import org.dromara.jpom.model.user.TriggerTokenLogBean;
 import org.dromara.jpom.model.user.UserModel;
@@ -198,8 +199,8 @@ public class TriggerTokenLogServer extends BaseDbService<TriggerTokenLogBean> im
                 while (true) {
                     Page page = new Page(pageNumber, 50);
                     Entity entity = new Entity();
-                    entity.set("type", triggerToken.typeName());
-                    entity.setFieldNames("id", "dataId");
+                    entity.set(DialectUtil.wrapField("type"), triggerToken.typeName());
+                    entity.setFieldNames(DialectUtil.wrapField("id"), DialectUtil.wrapField("dataId"));
                     PageResultDto<TriggerTokenLogBean> pageResult = this.listPage(entity, page);
                     if (pageResult.isEmpty()) {
                         break;

+ 4 - 2
modules/server/src/main/java/org/dromara/jpom/service/user/UserService.java

@@ -111,8 +111,10 @@ public class UserService extends BaseDbService<UserModel> {
         Integer status = userModel.getStatus();
         Assert.state(status == null || status == 1, ServerConst.ACCOUNT_LOCKED_TIP);
         String id = userModel.getId();
-        String sql = "select password from " + this.tableName + " where id=?";
-        List<Entity> query = super.query(sql, id);
+        Entity condition = Entity.create(this.tableName)
+            .set("id", id)
+            .setFieldNames("password");
+        List<Entity> query = super.queryList(condition);
         Entity first = CollUtil.getFirst(query);
         Assert.notEmpty(first, I18nMessageUtil.get("i18n.no_user_info.0355"));
         String password = (String) first.get("password");

+ 46 - 0
modules/server/src/main/java/org/dromara/jpom/socket/handler/KeyControl.java

@@ -0,0 +1,46 @@
+package org.dromara.jpom.socket.handler;
+
+import java.util.Arrays;
+
+/**
+ * 功能键枚举
+ *
+ * @author bwcx_jzy
+ * @since 2025/6/11
+ */
+public enum KeyControl {
+    KEY_TAB((byte) 9), // TAB
+    KEY_ETX((byte) 3), // Control + C
+    KEY_ENTER((byte) 13), // Enter
+    KEY_SEARCH((byte) 18), // Control + R
+    KEY_BACK((byte) 127), // 退格键
+    KEY_DELETE(new byte[]{27, 91, 51, 126}), // DELETE键
+    KEY_LEFT(new byte[]{27, 91, 68}), // 左
+    KEY_RIGHT(new byte[]{27, 91, 67}), // 右
+    KEY_UP(new byte[]{27, 91, 65}), // 上
+    KEY_DOWN(new byte[]{27, 91, 66}), // 下
+    KEY_HOME(new byte[]{27, 91, 72}),
+    KEY_END(new byte[]{27, 91, 70}),
+    KEY_FUNCTION(new byte[]{27, 91}), //其他功能键
+    KEY_INPUT(new byte[]{-1}); // 正常输入
+
+    private final byte[] control;
+
+    KeyControl(byte... control) {
+        this.control = control;
+    }
+
+    public static KeyControl getKeyControl(byte[] bytes) {
+        for (KeyControl value : KeyControl.values()) {
+            if (Arrays.equals(value.control, bytes)) {
+                return value;
+            }
+        }
+        // 其他功能键
+        if (Arrays.equals(KEY_FUNCTION.control, Arrays.copyOf(bytes, 2))) {
+            return KEY_FUNCTION;
+        }
+        // 正常输入
+        return KEY_INPUT;
+    }
+}

+ 202 - 0
modules/server/src/main/java/org/dromara/jpom/socket/handler/KeyEventCycle.java

@@ -0,0 +1,202 @@
+package org.dromara.jpom.socket.handler;
+
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.function.Consumer;
+
+/**
+ * 控制台案件事件处理
+ *
+ * @author bwcx_jzy
+ * @since 2025/6/11
+ */
+@Slf4j
+public class KeyEventCycle {
+    // 输入缓存
+    private StringBuffer buffer = new StringBuffer();
+    // 输入后是否接收返回字符串
+    private boolean inputReceive = false;
+    // TAB 输入暂停(处理Y/N确认)
+    private boolean tabInputPause = false;
+    // 光标位置
+    private int inputSelection = 0;
+    // 搜索状态,0未开始,1开始搜索,2搜索结束
+    private int searchState = 0;
+    @Setter
+    private Charset charset;
+    private KeyControl keyControl = KeyControl.KEY_END;
+    private Consumer<String> consumer;
+
+    /**
+     * 从控制台读取输入按键进行处理
+     *
+     * @param consumer 完整命令后输入回调
+     * @param bytes    输入按键
+     */
+    public void read(Consumer<String> consumer, byte... bytes) {
+        this.consumer = consumer;
+        String str = new String(bytes, charset);
+        if (keyControl == KeyControl.KEY_TAB && tabInputPause) {
+            if (str.equalsIgnoreCase("y") || str.equalsIgnoreCase("n")) {
+                tabInputPause = false;
+                return;
+            }
+        }
+        keyControl = KeyControl.getKeyControl(bytes);
+        if ((keyControl == KeyControl.KEY_INPUT || keyControl == KeyControl.KEY_FUNCTION) && !tabInputPause) {
+            buffer.insert(inputSelection, str);
+            inputSelection += str.length();
+        } else if (keyControl == KeyControl.KEY_ENTER) {
+            // 回车,结束当前输入周期
+            if (buffer.length() > 0 && searchState != 1) {
+                consumer.accept(buffer.toString());
+            } else if (searchState == 1) {
+                // Control + R结束
+                searchState = 2;
+            }
+            // 重置周期
+            buffer = new StringBuffer();
+            inputReceive = false;
+            inputSelection = 0;
+        } else if (keyControl == KeyControl.KEY_BACK) {
+            buffer.delete(Math.max(inputSelection - 1, 0), inputSelection);
+            inputSelection = Math.max(inputSelection - 1, 0);
+        } else if (keyControl == KeyControl.KEY_DELETE) {
+            buffer.delete(inputSelection, Math.min(inputSelection + 1, buffer.length()));
+        } else if (keyControl == KeyControl.KEY_LEFT) {
+            inputSelection = Math.max(inputSelection - 1, 0);
+        } else if (keyControl == KeyControl.KEY_RIGHT) {
+            inputSelection = Math.min(inputSelection + 1, buffer.length());
+        } else if (keyControl == KeyControl.KEY_HOME) {
+            inputSelection = 0;
+        } else if (keyControl == KeyControl.KEY_END) {
+            inputSelection = buffer.length();
+        } else if (keyControl == KeyControl.KEY_TAB) {
+            inputReceive = true;
+        } else if (keyControl == KeyControl.KEY_UP || keyControl == KeyControl.KEY_DOWN) {
+            // 清空命令缓冲
+            inputSelection = 0;
+            inputReceive = true;
+        } else if (keyControl == KeyControl.KEY_ETX) {
+            buffer = new StringBuffer();
+            inputSelection = 0;
+        } else if (keyControl == KeyControl.KEY_SEARCH) {
+            buffer = new StringBuffer();
+            searchState = 1;
+        }
+    }
+
+    /**
+     * 从SSH服务端接收字节
+     *
+     * @param bytes 字节
+     */
+    public void receive(byte... bytes) {
+        if (searchState == 2) {
+            // 处理搜索命令结束后,接收到ssh服务器返回的完整命令
+            int index = indexOf(bytes, new byte[]{27, 91, 75});
+            if (index > -1) {
+                bytes = Arrays.copyOf(bytes, index);
+            }
+            String str = new String(bytes, charset).split("# ")[1];
+            consumer.accept(str.trim());
+            searchState = 0;
+            return;
+        }
+        if (inputReceive) {
+            String str = new String(bytes, charset);
+            if (keyControl == KeyControl.KEY_UP || keyControl == KeyControl.KEY_DOWN) {
+                // 上下键只有第一条是正常的,后面的都是根据第一条进行退格删除再补充的。
+                // 8,8,8,99,100,32,47,112,114,50,111,99,47,
+                try {
+                    try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
+                        for (byte aByte : bytes) {
+                            if (aByte == 8) {
+                                // 首位是退格键,就执行删除末尾值
+                                buffer.deleteCharAt(Math.max(buffer.length() - 1, 0));
+                            } else if (aByte == 27) {
+                                // 遇到【逃离/取消】就跳出循环
+                                break;
+                            } else if (aByte != 0) {
+                                outputStream.write(aByte);
+                            }
+                        }
+                        buffer.append(new String(outputStream.toByteArray(), charset));
+                    }
+                    inputSelection = buffer.length();
+                } catch (Exception e) {
+                    log.error("", e);
+                }
+                return;
+            } else {
+                if (keyControl == KeyControl.KEY_TAB) {
+                    if (bytes[0] == 7) {
+                        // 接收到终端响铃,就删除响铃
+                        bytes = Arrays.copyOfRange(bytes, 1, bytes.length);
+                    }
+                    if (Arrays.equals(new byte[]{13, 10}, bytes)) {
+                        inputReceive = false;
+                        return;
+                    }
+                    // tab下文件很多
+                    if (str.contains("y or n")) {
+                        tabInputPause = true;
+                        inputReceive = false;
+                        return;
+                    }
+                    // cat 'hello word.txt'
+                    // cat hello\ word.txt
+                    if (str.split(" ").length > 1 && (!str.contains("'") && !str.contains("\\"))) {
+                        inputReceive = false;
+                        return;
+                    }
+                }
+                // 非上下键输入输入中,如果接受到数据就执行插入数据,根据当前光标位置执行插入
+                // 存在退格,就从光标位置开始删除
+                int backCount = 0;
+                for (byte aByte : bytes) {
+                    if (aByte == 8) {
+                        buffer.deleteCharAt(inputSelection - 1);
+                        backCount++;
+                    }
+                }
+                str = new String(Arrays.copyOfRange(bytes, 0, bytes.length - backCount), charset);
+                // #https://gitee.com/dromara/Jpom/issues/ICA57K
+                buffer.insert(inputSelection - 1, str);
+                inputSelection += str.length();
+            }
+        }
+        inputReceive = false;
+    }
+
+    /**
+     * 查找指定字节数组在原始字节数组中的位置
+     *
+     * @param originalArray   原始字节数组
+     * @param byteArrayToFind 要查找的字节数组
+     * @return 找到的位置索引,如果找不到返回 -1
+     */
+    private static int indexOf(byte[] originalArray, byte[] byteArrayToFind) {
+        // 遍历原始字节数组,查找匹配的起始位置
+        for (int i = 0; i <= originalArray.length - byteArrayToFind.length; i++) {
+            boolean match = true;
+            for (int j = 0; j < byteArrayToFind.length; j++) {
+                if (originalArray[i + j] != byteArrayToFind[j]) {
+                    match = false;
+                    break;
+                }
+            }
+            if (match) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+}
+
+

+ 5 - 231
modules/server/src/main/java/org/dromara/jpom/socket/handler/SshHandler.java

@@ -23,7 +23,6 @@ import com.alibaba.fastjson2.JSONValidator;
 import com.jcraft.jsch.ChannelShell;
 import com.jcraft.jsch.JSchException;
 import com.jcraft.jsch.Session;
-import lombok.Setter;
 import lombok.extern.slf4j.Slf4j;
 import org.dromara.jpom.common.i18n.I18nMessageUtil;
 import org.dromara.jpom.common.i18n.I18nThreadUtil;
@@ -42,7 +41,6 @@ import org.springframework.http.HttpHeaders;
 import org.springframework.web.socket.TextMessage;
 import org.springframework.web.socket.WebSocketSession;
 
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -52,7 +50,6 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.Consumer;
 
 /**
  * ssh 处理2
@@ -304,7 +301,11 @@ public class SshHandler extends BaseTerminalHandler {
                 //如果没有数据来,线程会一直阻塞在这个地方等待数据。
                 while ((i = inputStream.read(buffer)) != NioUtil.EOF) {
                     byte[] tempBytes = Arrays.copyOfRange(buffer, 0, i);
-                    keyEventCycle.receive(tempBytes);
+                    try {
+                        keyEventCycle.receive(tempBytes);
+                    } catch (Exception e) {
+                        log.warn("keyEventCycle.receive", e);
+                    }
                     sendBinary(session, new String(tempBytes, machineSshModel.charset()));
                 }
             } catch (Exception e) {
@@ -334,231 +335,4 @@ public class SshHandler extends BaseTerminalHandler {
         HANDLER_ITEM_CONCURRENT_HASH_MAP.remove(session.getId());
         SocketSessionUtil.close(session);
     }
-
-    /**
-     * 控制台案件事件处理
-     */
-    public static class KeyEventCycle {
-
-        // 输入缓存
-        private StringBuffer buffer = new StringBuffer();
-        // 输入后是否接收返回字符串
-        private boolean inputReceive = false;
-        // TAB 输入暂停(处理Y/N确认)
-        private boolean tabInputPause = false;
-        // 光标位置
-        private int inputSelection = 0;
-        // 搜索状态,0未开始,1开始搜索,2搜索结束
-        private int searchState = 0;
-        @Setter
-        private Charset charset;
-        private KeyControl keyControl = KeyControl.KEY_END;
-        private Consumer<String> consumer;
-
-        /**
-         * 从控制台读取输入按键进行处理
-         *
-         * @param consumer 完整命令后输入回调
-         * @param bytes    输入按键
-         */
-        public void read(Consumer<String> consumer, byte... bytes) {
-            this.consumer = consumer;
-            String str = new String(bytes, charset);
-            if (keyControl == KeyControl.KEY_TAB && tabInputPause) {
-                if (str.equalsIgnoreCase("y") || str.equalsIgnoreCase("n")) {
-                    tabInputPause = false;
-                    return;
-                }
-            }
-            keyControl = KeyControl.getKeyControl(bytes);
-            if ((keyControl == KeyControl.KEY_INPUT || keyControl == KeyControl.KEY_FUNCTION) && !tabInputPause) {
-                buffer.insert(inputSelection, str);
-                inputSelection += str.length();
-            } else if (keyControl == KeyControl.KEY_ENTER) {
-                // 回车,结束当前输入周期
-                if (buffer.length() > 0 && searchState != 1) {
-                    consumer.accept(buffer.toString());
-                } else if (searchState == 1) {
-                    // Control + R结束
-                    searchState = 2;
-                }
-                // 重置周期
-                buffer = new StringBuffer();
-                inputReceive = false;
-                inputSelection = 0;
-            } else if (keyControl == KeyControl.KEY_BACK) {
-                buffer.delete(Math.max(inputSelection - 1, 0), inputSelection);
-                inputSelection = Math.max(inputSelection - 1, 0);
-            } else if (keyControl == KeyControl.KEY_DELETE) {
-                buffer.delete(inputSelection, Math.min(inputSelection + 1, buffer.length()));
-            } else if (keyControl == KeyControl.KEY_LEFT) {
-                inputSelection = Math.max(inputSelection - 1, 0);
-            } else if (keyControl == KeyControl.KEY_RIGHT) {
-                inputSelection = Math.min(inputSelection + 1, buffer.length());
-            } else if (keyControl == KeyControl.KEY_HOME) {
-                inputSelection = 0;
-            } else if (keyControl == KeyControl.KEY_END) {
-                inputSelection = buffer.length();
-            } else if (keyControl == KeyControl.KEY_TAB) {
-                inputReceive = true;
-            } else if (keyControl == KeyControl.KEY_UP || keyControl == KeyControl.KEY_DOWN) {
-                // 清空命令缓冲
-                inputSelection = 0;
-                inputReceive = true;
-            } else if (keyControl == KeyControl.KEY_ETX) {
-                buffer = new StringBuffer();
-                inputSelection = 0;
-            } else if (keyControl == KeyControl.KEY_SEARCH) {
-                buffer = new StringBuffer();
-                searchState = 1;
-            }
-        }
-
-        /**
-         * 从SSH服务端接收字节
-         *
-         * @param bytes 字节
-         */
-        public void receive(byte... bytes) {
-            if (searchState == 2) {
-                // 处理搜索命令结束后,接收到ssh服务器返回的完整命令
-                int index = indexOf(bytes, new byte[]{27, 91, 75});
-                if (index > -1) {
-                    bytes = Arrays.copyOf(bytes, index);
-                }
-                String str = new String(bytes, charset).split("# ")[1];
-                consumer.accept(str.trim());
-                searchState = 0;
-                return;
-            }
-            if (inputReceive) {
-                String str = new String(bytes, charset);
-                if (keyControl == KeyControl.KEY_UP || keyControl == KeyControl.KEY_DOWN) {
-                    // 上下键只有第一条是正常的,后面的都是根据第一条进行退格删除再补充的。
-                    // 8,8,8,99,100,32,47,112,114,50,111,99,47,
-                    try {
-                        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
-                            for (byte aByte : bytes) {
-                                if (aByte == 8) {
-                                    // 首位是退格键,就执行删除末尾值
-                                    buffer.deleteCharAt(Math.max(buffer.length() - 1, 0));
-                                } else if (aByte == 27) {
-                                    // 遇到【逃离/取消】就跳出循环
-                                    break;
-                                } else if (aByte != 0) {
-                                    outputStream.write(aByte);
-                                }
-                            }
-                            buffer.append(new String(outputStream.toByteArray(), charset));
-                        }
-                        inputSelection = buffer.length();
-                    } catch (Exception e) {
-                        log.error("", e);
-                    }
-                    return;
-                } else {
-                    if (keyControl == KeyControl.KEY_TAB) {
-                        if (bytes[0] == 7) {
-                            // 接收到终端响铃,就删除响铃
-                            bytes = Arrays.copyOfRange(bytes, 1, bytes.length);
-                        }
-                        if (Arrays.equals(new byte[]{13, 10}, bytes)) {
-                            inputReceive = false;
-                            return;
-                        }
-                        // tab下文件很多
-                        if (str.contains("y or n")) {
-                            tabInputPause = true;
-                            inputReceive = false;
-                            return;
-                        }
-                        // cat 'hello word.txt'
-                        // cat hello\ word.txt
-                        if (str.split(" ").length > 1 && (!str.contains("'") && !str.contains("\\"))) {
-                            inputReceive = false;
-                            return;
-                        }
-                    }
-                    // 非上下键输入输入中,如果接受到数据就执行插入数据,根据当前光标位置执行插入
-                    // 存在退格,就从光标位置开始删除
-                    int backCount = 0;
-                    for (byte aByte : bytes) {
-                        if (aByte == 8) {
-                            buffer.deleteCharAt(inputSelection - 1);
-                            backCount++;
-                        }
-                    }
-                    str = new String(Arrays.copyOfRange(bytes, 0, bytes.length - backCount), charset);
-                    buffer.insert(inputSelection, str);
-                    inputSelection += str.length();
-                }
-            }
-            inputReceive = false;
-        }
-
-        /**
-         * 查找指定字节数组在原始字节数组中的位置
-         *
-         * @param originalArray 原始字节数组
-         * @param byteArrayToFind 要查找的字节数组
-         * @return 找到的位置索引,如果找不到返回 -1
-         */
-        private static int indexOf(byte[] originalArray, byte[] byteArrayToFind) {
-            // 遍历原始字节数组,查找匹配的起始位置
-            for (int i = 0; i <= originalArray.length - byteArrayToFind.length; i++) {
-                boolean match = true;
-                for (int j = 0; j < byteArrayToFind.length; j++) {
-                    if (originalArray[i + j] != byteArrayToFind[j]) {
-                        match = false;
-                        break;
-                    }
-                }
-                if (match) {
-                    return i;
-                }
-            }
-            return -1;
-        }
-
-    }
-
-    /**
-     * 功能键枚举
-     */
-    public enum KeyControl {
-        KEY_TAB((byte) 9), // TAB
-        KEY_ETX((byte) 3), // Control + C
-        KEY_ENTER((byte) 13), // Enter
-        KEY_SEARCH((byte) 18), // Control + R
-        KEY_BACK((byte) 127), // 退格键
-        KEY_DELETE(new byte[]{27, 91, 51, 126}), // DELETE键
-        KEY_LEFT(new byte[]{27, 91, 68}), // 左
-        KEY_RIGHT(new byte[]{27, 91, 67}), // 右
-        KEY_UP(new byte[]{27, 91, 65}), // 上
-        KEY_DOWN(new byte[]{27, 91, 66}), // 下
-        KEY_HOME(new byte[]{27, 91, 72}),
-        KEY_END(new byte[]{27, 91, 70}),
-        KEY_FUNCTION(new byte[]{27, 91}), //其他功能键
-        KEY_INPUT(new byte[]{-1}); // 正常输入
-
-        private final byte[] control;
-
-        KeyControl(byte... control) {
-            this.control = control;
-        }
-
-        public static KeyControl getKeyControl(byte[] bytes) {
-            for (KeyControl value : KeyControl.values()) {
-                if (Arrays.equals(value.control, bytes)) {
-                    return value;
-                }
-            }
-            // 其他功能键
-            if (Arrays.equals(KEY_FUNCTION.control, Arrays.copyOf(bytes, 2))) {
-                return KEY_FUNCTION;
-            }
-            // 正常输入
-            return KEY_INPUT;
-        }
-    }
 }

+ 7 - 3
modules/server/src/main/java/org/dromara/jpom/system/db/DataInitEvent.java

@@ -19,6 +19,7 @@ import org.dromara.jpom.common.ILoadEvent;
 import org.dromara.jpom.common.ServerConst;
 import org.dromara.jpom.common.i18n.I18nMessageUtil;
 import org.dromara.jpom.db.TableName;
+import org.dromara.jpom.dialect.DialectUtil;
 import org.dromara.jpom.model.BaseWorkspaceModel;
 import org.dromara.jpom.model.data.WorkspaceModel;
 import org.dromara.jpom.service.IStatusRecover;
@@ -99,7 +100,10 @@ public class DataInitEvent implements ILoadEvent, ICacheTask {
                 TableName tableName1 = parents.getAnnotation(TableName.class);
                 Assert.notNull(tableName1, I18nMessageUtil.get("i18n.parent_table_info_config_error.2f52") + aClass);
             }
-            String sql = "select workspaceId,count(1) as allCount from " + tableName.value() + " group by workspaceId";
+            String workspaceIdField = DialectUtil.wrapField("workspaceId");
+            String value = workspaceService.parseRealTableName(tableName);
+            String sql =StrUtil.format("select {},count(1) as allCount from {} group by {}",
+                workspaceIdField, value,workspaceIdField);
             List<Entity> query = workspaceService.query(sql);
             for (Entity entity : query) {
                 String workspaceId = (String) entity.get("workspaceId");
@@ -107,9 +111,9 @@ public class DataInitEvent implements ILoadEvent, ICacheTask {
                 if (workspaceIds.contains(workspaceId)) {
                     continue;
                 }
-                String format = StrUtil.format(I18nMessageUtil.get("i18n.table_error_workspace_data.9021"), I18nMessageUtil.get(tableName.nameKey()), tableName.value(), allCount, workspaceId);
+                String format = StrUtil.format(I18nMessageUtil.get("i18n.table_error_workspace_data.9021"), I18nMessageUtil.get(tableName.nameKey()), value, allCount, workspaceId);
                 log.error(format);
-                List<String> stringList = errorWorkspaceTable.computeIfAbsent(tableName.value(), s -> new ArrayList<>());
+                List<String> stringList = errorWorkspaceTable.computeIfAbsent(value, s -> new ArrayList<>());
                 stringList.add(format);
             }
         }

+ 37 - 20
modules/server/src/main/java/org/dromara/jpom/system/db/InitDb.java

@@ -93,7 +93,7 @@ public class InitDb implements DisposableBean, ILoadEvent {
         //
         log.debug(I18nMessageUtil.get("i18n.need_execute_pre_events.b848"), BEFORE_CALLBACK.size());
         BEFORE_CALLBACK.forEach(Runnable::run);
-        IStorageService storageService = StorageServiceFactory.get();
+        IStorageService storageService = StorageServiceFactory.getInstance().get();
         log.info(I18nMessageUtil.get("i18n.start_loading_database.b040"), dbExtConfig.getMode());
         DSFactory dsFactory = storageService.init(dbExtConfig);
         final String[] sqlFileNow = {StrUtil.EMPTY};
@@ -107,7 +107,7 @@ public class InitDb implements DisposableBean, ILoadEvent {
                 String filename = resource.getFilename();
                 List<String> list = StrUtil.splitTrim(filename, StrUtil.DOT);
                 String modeType = CollUtil.get(list, 1);
-                return StrUtil.equalsAnyIgnoreCase(modeType, "all", StorageServiceFactory.getMode().name());
+                return StrUtil.equalsAnyIgnoreCase(modeType, "all", StorageServiceFactory.getInstance().getMode().name());
             };
             List<Resource> resourceList = Arrays.stream(csvResources).filter(filter).collect(Collectors.toList());
             //
@@ -138,10 +138,10 @@ public class InitDb implements DisposableBean, ILoadEvent {
             DataSource dataSource = dsFactory.getDataSource();
             // 第一次初始化数据库
             // 加载 sql 变更记录,避免重复执行
-            Set<String> executeSqlLog = StorageServiceFactory.loadExecuteSqlLog();
+            Set<String> executeSqlLog = StorageServiceFactory.getInstance().loadExecuteSqlLog();
             tryInitSql(dbExtConfig.getMode(), listMap, executeSqlLog, dataSource, s -> sqlFileNow[0] = s);
             //
-            StorageServiceFactory.saveExecuteSqlLog(executeSqlLog);
+            StorageServiceFactory.getInstance().saveExecuteSqlLog(executeSqlLog);
             // 执行回调方法
             log.debug(I18nMessageUtil.get("i18n.need_execute_callbacks.b708"), AFTER_CALLBACK.size());
             long count = AFTER_CALLBACK.entrySet()
@@ -187,6 +187,9 @@ public class InitDb implements DisposableBean, ILoadEvent {
         Optional.ofNullable(listMap.get("table")).ifPresent(resources -> {
             for (Resource resource : resources) {
                 String sql = StorageTableFactory.initTable(resource);
+                if (mode.equals(DbExtConfig.Mode.DAMENG)) {
+                    sql = sql.toUpperCase();
+                }
                 this.executeSql(sql, resource.getFilename(), mode, executeSqlLog, dataSource, eachSql);
             }
         });
@@ -196,12 +199,18 @@ public class InitDb implements DisposableBean, ILoadEvent {
         Optional.ofNullable(listMap.get("alter")).ifPresent(resources -> {
             for (Resource resource : resources) {
                 String sql = StorageTableFactory.initAlter(resource);
+                if (mode.equals(DbExtConfig.Mode.DAMENG)) {
+                    sql = sql.toUpperCase();
+                }
                 this.executeSql(sql, resource.getFilename(), mode, executeSqlLog, dataSource, eachSql);
             }
         });
         Optional.ofNullable(listMap.get("index")).ifPresent(resources -> {
             for (Resource resource : resources) {
                 String sql = StorageTableFactory.initIndex(resource);
+                if (mode.equals(DbExtConfig.Mode.DAMENG)) {
+                    sql = sql.toUpperCase();
+                }
                 this.executeSql(sql, resource.getFilename(), mode, executeSqlLog, dataSource, eachSql);
             }
         });
@@ -247,18 +256,18 @@ public class InitDb implements DisposableBean, ILoadEvent {
         //
         GlobalPruneTimer.INSTANCE.shutdownNow();
         // 关闭数据库
-        IoUtil.close(StorageServiceFactory.get());
+        IoUtil.close(StorageServiceFactory.getInstance().get());
     }
 
     private void prepareCallback(Environment environment) {
         Opt.ofNullable(environment.getProperty("rest:load_init_db")).ifPresent(s -> {
             // 重新执行数据库初始化操作,一般用于手动修改数据库字段错误后,恢复默认的字段
-            StorageServiceFactory.clearExecuteSqlLog();
+            StorageServiceFactory.getInstance().clearExecuteSqlLog();
         });
         Opt.ofNullable(environment.getProperty("recover:h2db")).ifPresent(s -> {
             // 恢复数据库,一般用于非正常关闭程序导致数据库奔溃,执行恢复数据逻辑
             try {
-                this.recoverSqlFile = StorageServiceFactory.get().recoverDb();
+                this.recoverSqlFile = StorageServiceFactory.getInstance().get().recoverDb();
             } catch (Exception e) {
                 throw new JpomRuntimeException("Failed to restore database", e);
             }
@@ -284,7 +293,7 @@ public class InitDb implements DisposableBean, ILoadEvent {
             // 删除掉旧数据
             this.addBeforeCallback(() -> {
                 try {
-                    String dbFiles = StorageServiceFactory.get().deleteDbFiles();
+                    String dbFiles = StorageServiceFactory.getInstance().get().deleteDbFiles();
                     if (dbFiles != null) {
                         String s1 = "自动备份数据文件到路径";
                         log.info("{}:{}", s1, dbFiles);
@@ -304,27 +313,35 @@ public class InitDb implements DisposableBean, ILoadEvent {
                 throw new JpomRuntimeException(StrUtil.format(I18nMessageUtil.get("i18n.incorrect_mode_for_migration.caef"), targetMode));
             }
             // 都提前清理
-            StorageServiceFactory.clearExecuteSqlLog();
+            StorageServiceFactory.getInstance().clearExecuteSqlLog();
             //
             String user = environment.getProperty("h2-user");
             String url = environment.getProperty("h2-url");
             String pass = environment.getProperty("h2-pass");
             this.addCallback(I18nMessageUtil.get("i18n.migrate_data.f556"), () -> {
                 //
-                StorageServiceFactory.migrateH2ToNow(dbExtConfig, url, user, pass, targetMode);
+                StorageServiceFactory.getInstance().migrateH2ToNow(dbExtConfig, url, user, pass, targetMode);
                 return false;
             });
             log.info(I18nMessageUtil.get("i18n.start_waiting_for_data_migration.e76f"));
         };
-        Opt.ofNullable(environment.getProperty("h2-migrate-mysql")).ifPresent(s -> {
-            migrateOpr.accept(DbExtConfig.Mode.MYSQL);
-        });
-        Opt.ofNullable(environment.getProperty("h2-migrate-postgresql")).ifPresent(s -> {
-            migrateOpr.accept(DbExtConfig.Mode.POSTGRESQL);
-        });
-        Opt.ofNullable(environment.getProperty("h2-migrate-mariadb")).ifPresent(s -> {
-            migrateOpr.accept(DbExtConfig.Mode.MARIADB);
-        });
+        for (DbExtConfig.Mode mode : DbExtConfig.Mode.values()) {
+            if (mode == DbExtConfig.Mode.H2) {
+                continue;
+            }
+            Opt.ofNullable(environment.getProperty("h2-migrate-" + mode.name().toLowerCase())).ifPresent(s -> {
+                migrateOpr.accept(mode);
+            });
+        }
+//        Opt.ofNullable(environment.getProperty("h2-migrate-mysql")).ifPresent(s -> {
+//            migrateOpr.accept(DbExtConfig.Mode.MYSQL);
+//        });
+//        Opt.ofNullable(environment.getProperty("h2-migrate-postgresql")).ifPresent(s -> {
+//            migrateOpr.accept(DbExtConfig.Mode.POSTGRESQL);
+//        });
+//        Opt.ofNullable(environment.getProperty("h2-migrate-mariadb")).ifPresent(s -> {
+//            migrateOpr.accept(DbExtConfig.Mode.MARIADB);
+//        });
     }
 
     private void importH2Sql(Environment environment, String importH2Sql) {
@@ -335,7 +352,7 @@ public class InitDb implements DisposableBean, ILoadEvent {
                 throw new JpomRuntimeException(StrUtil.format("sql file does not exist :{}", sqlPath));
             }
             //
-            Opt.ofNullable(environment.getProperty("transform-sql")).ifPresent(s -> StorageServiceFactory.get().transformSql(file));
+            Opt.ofNullable(environment.getProperty("transform-sql")).ifPresent(s -> StorageServiceFactory.getInstance().get().transformSql(file));
             //
             log.info("开始导入数据:{}", sqlPath);
             boolean flag = backupInfoService.restoreWithSql(sqlPath);

+ 17 - 0
modules/server/src/main/resources/application-dameng.yml

@@ -0,0 +1,17 @@
+#
+# Copyright (c) 2019 Of Him Code Technology Studio
+# Jpom is licensed under Mulan PSL v2.
+# You can use this software according to the terms and conditions of the Mulan PSL v2.
+# You may obtain a copy of Mulan PSL v2 at:
+# 			http://license.coscl.org.cn/MulanPSL2
+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+# See the Mulan PSL v2 for more details.
+#
+
+jpom:
+  db:
+    # 数据库默认 支持 :H2、MYSQL、MARIADB、POSTGRESQL、DAMENG
+    mode: DAMENG
+    url: jdbc:dm://192.168.30.29:5236?schema=JPOM
+    user-name: SYSDBA
+    user-pwd: SYSDBA001

+ 1 - 1
modules/server/src/main/resources/application-mariadb.yml

@@ -10,7 +10,7 @@
 
 jpom:
   db:
-    # 数据库默认 支持 :H2、MYSQL、MARIADB、POSTGRESQL
+    # 数据库默认 支持 :H2、MYSQL、MARIADB、POSTGRESQL、DAMENG
     mode: MARIADB
     url: jdbc:mariadb://127.0.0.1:3309/jpom
     # 数据库账号 默认 jpom

+ 1 - 1
modules/server/src/main/resources/application-mysql.yml

@@ -10,7 +10,7 @@
 
 jpom:
   db:
-    # 数据库默认 支持 :H2、MYSQL、MARIADB、POSTGRESQL
+    # 数据库默认 支持 :H2、MYSQL、MARIADB、POSTGRESQL、DAMENG
     mode: MYSQL
     url: jdbc:mysql://127.0.0.1:3306/jpom
     # 数据库账号 默认 jpom

+ 1 - 1
modules/server/src/main/resources/application-postgresql.yml

@@ -10,7 +10,7 @@
 
 jpom:
   db:
-    # 数据库默认 支持 :H2、MYSQL、MARIADB、POSTGRESQL
+    # 数据库默认 支持 :H2、MYSQL、MARIADB、POSTGRESQL、DAMENG
     mode: POSTGRESQL
     url: jdbc:postgresql://127.0.0.1:5432/jpom
     # 数据库账号 默认 jpom

+ 16 - 6
modules/server/src/main/resources/application.yml

@@ -74,7 +74,7 @@ jpom:
   # 查看日志时初始读取最后多少行(默认10,0不读取)
   init-read-line: 10
   db:
-    # 数据库默认 支持 :H2、MYSQL、MARIADB、POSTGRESQL
+    # 数据库默认 支持 :H2、MYSQL、MARIADB、POSTGRESQL、DAMENG
     mode: H2
     # 日志存储条数,将自动清理旧数据,配置小于等于零则不清理
     log-storage-count: 10000
@@ -96,7 +96,9 @@ jpom:
     max-wait: 10
     min-idle: 1
     show-sql: false
-  # 构建相关配置
+    # 表名前缀
+  #    table-prefix: jpom_
+  # 构建相关配置s
   build:
     # 最多保存多少份历史记录
     max-history-count: 1000
@@ -127,6 +129,13 @@ jpom:
       monitor-cron: 0 0/1 * * * ?
       disable-monitor-group-name:
         - 禁用监控
+    # ftp 资产
+    ftp:
+      # 监控频率
+      monitor-cron: 0 0/1 * * * ?
+      # 指定分组不启用监控功能(如果想禁用所有配置 * 即可)
+      disable-monitor-group-name:
+        - 禁用监控
     docker:
       monitor-cron: 0 0/1 * * * ?
 server:
@@ -154,10 +163,11 @@ server:
     connection-timeout: 10M
 
 spring:
-  #  profiles:
-  #    active: mysql
-  #    active: mariadb
-  #    active: postgresql
+  profiles:
+    #    active: mysql
+    #    active: mariadb
+    #    active: postgresql
+    #    active: dameng
 
   web:
     resources:

+ 10 - 1
modules/server/src/main/resources/config_default/application.yml

@@ -74,7 +74,7 @@ jpom:
   # 查看日志时初始读取最后多少行(默认10,0不读取)
   init-read-line: 10
   db:
-    # 数据库默认 支持 :H2、MYSQL、MARIADB、POSTGRESQL
+    # 数据库默认 支持 :H2、MYSQL、MARIADB、POSTGRESQL、DAMENG
     mode: H2
     # 日志存储条数,将自动清理旧数据,配置小于等于零则不清理
     log-storage-count: 10000
@@ -97,6 +97,8 @@ jpom:
     min-idle: 1
     # 控制台是否打印 sql 信息
     show-sql: false
+      # 表名前缀
+    table-prefix:
   # 构建相关配置
   build:
     # 最多保存多少份历史记录
@@ -131,6 +133,13 @@ jpom:
       # 指定分组不启用监控功能(如果想禁用所有配置 * 即可)
       disable-monitor-group-name:
         - 禁用监控
+    # ftp 资产
+    ftp:
+      # 监控频率
+      monitor-cron: 0 0/1 * * * ?
+      # 指定分组不启用监控功能(如果想禁用所有配置 * 即可)
+      disable-monitor-group-name:
+        - 禁用监控
     # docker 资产
     docker:
       # 监控频率

+ 11 - 0
modules/server/src/main/resources/menus/zh-CN/index.json

@@ -74,6 +74,17 @@
       }
     ]
   },
+  {
+    "title": "FTP管理",
+    "icon_v3": "code",
+    "id": "ftpManager",
+    "childs": [
+      {
+        "title": "FTP列表",
+        "id": "ftpList"
+      }
+    ]
+  },
   {
     "title": "脚本管理",
     "icon_v3": "file-text",

+ 5 - 0
modules/server/src/main/resources/menus/zh-CN/system.json

@@ -19,6 +19,11 @@
         "title": "SSH管理",
         "role": "system"
       },
+      {
+        "id": "machine_ftp_info",
+        "title": "FTP管理",
+        "role": "system"
+      },
       {
         "id": "machine_docker_info",
         "title": "Docker管理",

+ 2 - 0
modules/server/src/main/resources/sql-view/table.all.v1.1.csv

@@ -49,6 +49,7 @@ MACHINE_NODE_STAT_LOG,networkDelay,Integer,,0,false,false,网络耗时,
 MACHINE_NODE_STAT_LOG,monitorTime,Long,,,true,false,监控的时间,
 MACHINE_NODE_STAT_LOG,netTxBytes,Long,,,false,false,"每秒发送的KB数,rxkB/s",
 MACHINE_NODE_STAT_LOG,netRxBytes,Long,,,false,false,"每秒接收的KB数,rxkB/s",
+
 MACHINE_SSH_INFO,id,String,50,,true,true,id,ssh信息表
 MACHINE_SSH_INFO,createTimeMillis,Long,,,false,false,数据创建时间,
 MACHINE_SSH_INFO,modifyTimeMillis,Long,,,false,false,数据修改时间,
@@ -65,6 +66,7 @@ MACHINE_SSH_INFO,connectType,String,10,,false,false,连接方式,
 MACHINE_SSH_INFO,timeout,Integer,,0,false,false,节点超时时间,
 MACHINE_SSH_INFO,status,TINYINT,,,true,false,状态{0,无法连接,1 正常},
 MACHINE_SSH_INFO,statusMsg,TEXT,,,false,false,状态消息,
+
 MACHINE_DOCKER_INFO,id,String,50,,true,true,id,机器DOCKER信息
 MACHINE_DOCKER_INFO,createTimeMillis,Long,,,false,false,数据创建时间,
 MACHINE_DOCKER_INFO,modifyTimeMillis,Long,,,false,false,数据修改时间,

+ 39 - 0
modules/server/src/main/resources/sql-view/table.all.v1.2.csv

@@ -40,6 +40,25 @@ SCRIPT_LIBRARY,script,text,,,false,false,描述,
 SCRIPT_LIBRARY,machineIds,text,,,false,false,关联的机器节点,
 SCRIPT_LIBRARY,version,String,50,,true,false,版本,
 
+MACHINE_FTP_INFO,id,String,50,,true,true,id,ftp信息表
+MACHINE_FTP_INFO,createTimeMillis,Long,,,false,false,数据创建时间,
+MACHINE_FTP_INFO,modifyTimeMillis,Long,,,false,false,数据修改时间,
+MACHINE_FTP_INFO,modifyUser,String,50,,false,false,修改人,
+MACHINE_FTP_INFO,name,String,50,,false,false,名称,
+MACHINE_FTP_INFO,groupName,String,50,,true,false,分组名称,
+MACHINE_FTP_INFO,host,String,100,,true,false,ssh host IP,
+MACHINE_FTP_INFO,port,Integer,,,true,false,端口,
+MACHINE_FTP_INFO,user,String,100,,true,false,用户,
+MACHINE_FTP_INFO,password,String,100,,false,false,密码,
+MACHINE_FTP_INFO,charset,String,100,,false,false,编码格式,
+MACHINE_FTP_INFO,serverLanguageCode,String,50,,false,false,设置服务器语言,
+MACHINE_FTP_INFO,systemKey,String,10,,false,false,系统关键词,
+MACHINE_FTP_INFO,allowEditSuffix,TEXT,,,false,false,允许编辑的后缀文件,
+MACHINE_FTP_INFO,timeout,Integer,,0,false,false,超时时间,
+MACHINE_FTP_INFO,mode,String,10,,true,false,主动模式或者被动模式,
+MACHINE_FTP_INFO,status,TINYINT,,,true,false,状态{0,无法连接,1 正常},
+MACHINE_FTP_INFO,statusMsg,TEXT,,,false,false,状态消息,
+
 FILE_RELEASE_TASK_TEMPLATE,id,String,50,,true,true,id,文件发布任务记录
 FILE_RELEASE_TASK_TEMPLATE,createTimeMillis,Long,,,true,false,数据创建时间,
 FILE_RELEASE_TASK_TEMPLATE,modifyTimeMillis,Long,,,false,false,数据修改时间,
@@ -49,3 +68,23 @@ FILE_RELEASE_TASK_TEMPLATE,name,String,255,,true,false,名称,
 FILE_RELEASE_TASK_TEMPLATE,templateTag,String,50,,true,false,模板标记,
 FILE_RELEASE_TASK_TEMPLATE,fileType,TINYINT,,,false,false,文件类型,
 FILE_RELEASE_TASK_TEMPLATE,data,TEXT,,,false,false,发布之前的脚本,
+
+FTP_INFO,id,String,50,,true,true,id,ftp信息表
+FTP_INFO,createTimeMillis,Long,,,false,false,数据创建时间,
+FTP_INFO,modifyTimeMillis,Long,,,false,false,数据修改时间,
+FTP_INFO,modifyUser,String,50,,false,false,修改人,
+FTP_INFO,workspaceId,String,50,,true,false,所属工作空间,
+FTP_INFO,name,String,50,,false,false,名称,
+FTP_INFO,host,String,100,,true,false,ssh host IP,
+FTP_INFO,port,Integer,,,true,false,端口,
+FTP_INFO,user,String,100,,true,false,用户,
+FTP_INFO,password,String,100,,false,false,密码,
+FTP_INFO,charset,String,100,,false,false,编码格式,
+FTP_INFO,serverLanguageCode,String,50,,false,false,设置服务器语言,
+FTP_INFO,systemKey,String,10,,false,false,系统关键词,
+FTP_INFO,fileDirs,TEXT,,,false,false,文件目录,
+FTP_INFO,allowEditSuffix,TEXT,,,false,false,允许编辑的后缀文件,
+FTP_INFO,group,String,10,,true,false,分组,
+FTP_INFO,machineFtpId,String,50,,true,false,机器ftpid,
+
+

+ 3 - 3
modules/server/src/test/java/org/dromara/jpom/H2TableToCsv2Test.java

@@ -50,12 +50,12 @@ public class H2TableToCsv2Test extends ApplicationStartTest {
         Set<Class<?>> classes = ClassUtil.scanPackageByAnnotation("org.dromara.jpom", TableName.class);
         Map<String, Map<String, Field>> TABLE_NAME_MAP = CollStreamUtil.toMap(classes, aClass -> {
             TableName tableName = aClass.getAnnotation(TableName.class);
-            return tableName.value();
+            return StorageServiceFactory.getInstance().parseRealTableName(tableName);
         }, aClass -> {
             Map<String, Field> fieldMap = ReflectUtil.getFieldMap(aClass);
             return new CaseInsensitiveMap<>(fieldMap);
         });
-        IStorageService iStorageService = StorageServiceFactory.get();
+        IStorageService iStorageService = StorageServiceFactory.getInstance().get();
         List<Entity> query = Db.use(iStorageService.getDsFactory().getDataSource()).query("SELECT * FROM INFORMATION_SCHEMA.COLUMNS  where TABLE_SCHEMA='PUBLIC'");
 
         Map<String, List<JSONObject>> listMap = query.stream().map(entity -> {
@@ -120,7 +120,7 @@ public class H2TableToCsv2Test extends ApplicationStartTest {
 
     private String tableRemarks(String tableName) throws SQLException {
         String sql = "SELECT * FROM INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA='PUBLIC' and  TABLE_NAME=?";
-        IStorageService iStorageService = StorageServiceFactory.get();
+        IStorageService iStorageService = StorageServiceFactory.getInstance().get();
         List<Entity> query = Db.use(iStorageService.getDsFactory().getDataSource()).query(sql, tableName);
         Entity entity = CollUtil.getFirst(query);
         return entity.getStr("REMARKS");

+ 2 - 2
modules/server/src/test/java/org/dromara/jpom/service/h2db/H2ToolTest.java

@@ -47,7 +47,7 @@ public class H2ToolTest extends ApplicationStartTest {
     @Test
     public void testH2ShellForBackupSQL() throws SQLException {
         // 数据源参数
-        String url = StorageServiceFactory.get().dbUrl();
+        String url = StorageServiceFactory.getInstance().get().dbUrl();
 
         String user = dbExtConfig.userName();
         String pass = dbExtConfig.userPwd();
@@ -125,7 +125,7 @@ public class H2ToolTest extends ApplicationStartTest {
     @Test
     public void testH2DropAllObjects() throws SQLException {
         // 数据源参数
-        String url = StorageServiceFactory.get().dbUrl();
+        String url = StorageServiceFactory.getInstance().get().dbUrl();
 
         String user = dbExtConfig.userName();
         String pass = dbExtConfig.userPwd();

+ 3 - 2
modules/storage-module/pom.xml

@@ -16,7 +16,7 @@
     <parent>
         <artifactId>jpom-parent</artifactId>
         <groupId>org.dromara.jpom</groupId>
-        <version>2.11.12</version>
+        <version>2.11.12.1</version>
         <relativePath>../../pom.xml</relativePath>
     </parent>
     <packaging>pom</packaging>
@@ -26,9 +26,10 @@
         <module>storage-module-mysql</module>
         <module>storage-module-postgresql</module>
         <module>storage-module-mariadb</module>
+        <module>storage-module-dameng</module>
     </modules>
     <modelVersion>4.0.0</modelVersion>
-    <version>2.11.12</version>
+    <version>2.11.12.1</version>
     <groupId>org.dromara.jpom.storage-module</groupId>
     <artifactId>jpom-storage-module-parent</artifactId>
     <name>Jpom storage module</name>

+ 1 - 1
modules/storage-module/storage-module-common/pom.xml

@@ -17,7 +17,7 @@
     <parent>
         <groupId>org.dromara.jpom.storage-module</groupId>
         <artifactId>jpom-storage-module-parent</artifactId>
-        <version>2.11.12</version>
+        <version>2.11.12.1</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
 

+ 11 - 4
modules/storage-module/storage-module-common/src/main/java/org/dromara/jpom/db/BaseDbCommonService.java

@@ -65,13 +65,20 @@ public abstract class BaseDbCommonService<T> {
     protected final Class<T> tClass;
     protected final DbExtConfig.Mode dbMode;
 
+    protected DbExtConfig extConfig;
+
     @SuppressWarnings("unchecked")
     public BaseDbCommonService() {
         this.tClass = (Class<T>) TypeUtil.getTypeArgument(this.getClass());
         TableName annotation = tClass.getAnnotation(TableName.class);
         Assert.notNull(annotation, I18nMessageUtil.get("i18n.configure_table_name.f6fd"));
-        this.tableName = annotation.value();
-        this.dbMode = SpringUtil.getBean(DbExtConfig.class).getMode();
+        this.extConfig = SpringUtil.getBean(DbExtConfig.class);
+        this.tableName = parseRealTableName(annotation);
+        this.dbMode = extConfig.getMode();
+    }
+
+    public String parseRealTableName(TableName annotation) {
+        return StorageServiceFactory.getInstance().parseRealTableName(annotation);
     }
 
     public String getDataDesc() {
@@ -81,7 +88,7 @@ public abstract class BaseDbCommonService<T> {
     }
 
     protected DataSource getDataSource() {
-        DSFactory dsFactory = StorageServiceFactory.get().getDsFactory();
+        DSFactory dsFactory = StorageServiceFactory.getInstance().get().getDsFactory();
         return dsFactory.getDataSource();
     }
 
@@ -406,6 +413,6 @@ public abstract class BaseDbCommonService<T> {
      * @param e 异常
      */
     private JpomRuntimeException warpException(Exception e) {
-        return StorageServiceFactory.get().warpException(e);
+        return StorageServiceFactory.getInstance().get().warpException(e);
     }
 }

+ 12 - 2
modules/storage-module/storage-module-common/src/main/java/org/dromara/jpom/db/DbExtConfig.java

@@ -94,6 +94,10 @@ public class DbExtConfig implements InitializingBean {
      * @see cn.hutool.db.sql.SqlLog#KEY_SHOW_SQL
      */
     private Boolean showSql = false;
+    /**
+     * 表前缀
+     */
+    private String tablePrefix = "";
 
     public String userName() {
         return StrUtil.emptyToDefault(this.userName, DbExtConfig.DEFAULT_USER_OR_AUTHORIZATION);
@@ -132,7 +136,9 @@ public class DbExtConfig implements InitializingBean {
 
     @Override
     public void afterPropertiesSet() throws Exception {
-        StorageServiceFactory.setMode(this.getMode());
+        StorageServiceFactory instance = StorageServiceFactory.getInstance();
+        instance.setMode(this.getMode());
+        instance.setTablePrefix(this.getTablePrefix());
     }
 
     public enum Mode {
@@ -151,6 +157,10 @@ public class DbExtConfig implements InitializingBean {
         /**
          * Mariadb
          */
-        MARIADB
+        MARIADB,
+        /**
+         * Dameng
+         */
+        DAMENG,
     }
 }

+ 64 - 51
modules/storage-module/storage-module-common/src/main/java/org/dromara/jpom/db/StorageServiceFactory.java

@@ -21,7 +21,9 @@ import cn.hutool.db.*;
 import cn.hutool.db.ds.DSFactory;
 import cn.hutool.db.sql.Wrapper;
 import cn.hutool.setting.Setting;
+import lombok.Getter;
 import lombok.Lombok;
+import lombok.Setter;
 import lombok.extern.slf4j.Slf4j;
 import org.dromara.jpom.common.i18n.I18nMessageUtil;
 import org.dromara.jpom.dialect.DialectUtil;
@@ -53,25 +55,42 @@ public class StorageServiceFactory {
     /**
      * 当前运行模式
      */
-    private static DbExtConfig.Mode mode;
+    @Getter
+    @Setter
+    private DbExtConfig.Mode mode;
+
+    @Getter
+    @Setter
+    private String tablePrefix;
+
+    private StorageServiceFactory() {
+
+    }
+
+    public static StorageServiceFactory getInstance() {
+        return SingletonI.INSTANCE;
+    }
 
     /**
-     * 配置当前数据库 模式
-     *
-     * @param mode mode
+     * 单例模式
      */
-    public static void setMode(DbExtConfig.Mode mode) {
-        StorageServiceFactory.mode = mode;
+    private static final class SingletonI {
+        private static final StorageServiceFactory INSTANCE = new StorageServiceFactory();
+    }
+
+    public String parseRealTableName(TableName annotation) {
+        return StrUtil.emptyToDefault(this.getTablePrefix(), StrUtil.EMPTY) + annotation.value();
     }
 
-    public static DbExtConfig.Mode getMode() {
-        return mode;
+    public String parseRealTableName(String tableName) {
+        return StrUtil.emptyToDefault(this.getTablePrefix(), StrUtil.EMPTY) + tableName;
     }
 
+
     /**
      * 将数据迁移到当前环境
      */
-    public static void migrateH2ToNow(DbExtConfig dbExtConfig, String h2Url, String h2User, String h2Pass, DbExtConfig.Mode targetNode) {
+    public void migrateH2ToNow(DbExtConfig dbExtConfig, String h2Url, String h2User, String h2Pass, DbExtConfig.Mode targetNode) {
         log.info(I18nMessageUtil.get("i18n.start_migrating_h2_data_to.f478"), dbExtConfig.getMode());
         Assert.notNull(mode, I18nMessageUtil.get("i18n.target_database_info_not_specified.2ff6"));
         try {
@@ -86,7 +105,7 @@ public class StorageServiceFactory {
             log.info(I18nMessageUtil.get("i18n.h2_connection_successful.11f3"));
             // 设置默认备份 SQL 的文件地址
             String fileName = LocalDateTimeUtil.format(LocalDateTimeUtil.now(), DatePattern.PURE_DATETIME_PATTERN);
-            File file = FileUtil.file(StorageServiceFactory.dbLocalPath(), DbExtConfig.BACKUP_DIRECTORY_NAME, fileName + DbExtConfig.SQL_FILE_SUFFIX);
+            File file = FileUtil.file(StorageServiceFactory.getInstance().dbLocalPath(), DbExtConfig.BACKUP_DIRECTORY_NAME, fileName + DbExtConfig.SQL_FILE_SUFFIX);
             String backupSqlPath = FileUtil.getAbsolutePath(file);
             Setting setting = h2StorageService.createSetting(dbExtConfig, h2Url, h2User, h2Pass);
             // 数据源参数
@@ -101,17 +120,14 @@ public class StorageServiceFactory {
             nowDsFactory.getDataSource();
             log.info(I18nMessageUtil.get("i18n.connection_successful.0515"), dbExtConfig.getMode(), dbExtConfig.getUrl());
             Set<Class<?>> classes = ClassUtil.scanPackageByAnnotation("org.dromara.jpom", TableName.class);
-            classes = classes.stream()
-                .filter(aClass -> {
-                    TableName tableName = aClass.getAnnotation(TableName.class);
-                    DbExtConfig.Mode[] modes = tableName.modes();
-                    if (ArrayUtil.isEmpty(modes)) {
-                        return true;
-                    }
-                    return ArrayUtil.contains(modes, dbExtConfig.getMode());
-                })
-                .sorted((o1, o2) -> StrUtil.compare(o1.getSimpleName(), o2.getSimpleName(), false))
-                .collect(Collectors.toCollection(LinkedHashSet::new));
+            classes = classes.stream().filter(aClass -> {
+                TableName tableName = aClass.getAnnotation(TableName.class);
+                DbExtConfig.Mode[] modes = tableName.modes();
+                if (ArrayUtil.isEmpty(modes)) {
+                    return true;
+                }
+                return ArrayUtil.contains(modes, dbExtConfig.getMode());
+            }).sorted((o1, o2) -> StrUtil.compare(o1.getSimpleName(), o2.getSimpleName(), false)).collect(Collectors.toCollection(LinkedHashSet::new));
             log.info(I18nMessageUtil.get("i18n.prepare_to_migrate_data.f251"));
             int total = 0;
             for (Class<?> aClass : classes) {
@@ -129,18 +145,17 @@ public class StorageServiceFactory {
         }
     }
 
-    private static int migrateH2ToNowItem(Class<?> aClass, DSFactory h2DsFactory, DSFactory targetDsFactory, DbExtConfig.Mode targetNode) throws SQLException {
+    private int migrateH2ToNowItem(Class<?> aClass, DSFactory h2DsFactory, DSFactory targetDsFactory, DbExtConfig.Mode targetNode) throws SQLException {
         TableName tableName = aClass.getAnnotation(TableName.class);
         Wrapper targetModeWrapper = DialectUtil.getDialectByMode(targetNode).getWrapper();
-        Set<String> boolFieldSet = Arrays.stream(ReflectUtil.getFields(aClass, field -> Boolean.class.equals(field.getType()) || boolean.class.equals(field.getType())))
-            .map(Field::getName)
-            .collect(Collectors.toSet());
+        Set<String> boolFieldSet = Arrays.stream(ReflectUtil.getFields(aClass, field -> Boolean.class.equals(field.getType()) || boolean.class.equals(field.getType()))).map(Field::getName).collect(Collectors.toSet());
 
         String tableDesc = I18nMessageUtil.get(tableName.nameKey());
-        log.info(I18nMessageUtil.get("i18n.start_migrating.20d6"), tableDesc, tableName.value());
+        String value = parseRealTableName(tableName);
+        log.info(I18nMessageUtil.get("i18n.start_migrating.20d6"), tableDesc, value);
         int total = 0;
         while (true) {
-            Entity where = Entity.create(tableName.value());
+            Entity where = Entity.create(value);
             PageResult<Entity> pageResult;
             Db db = Db.use(h2DsFactory.getDataSource(), DialectUtil.getH2Dialect());
             Page page = new Page(1, 200);
@@ -149,23 +164,21 @@ public class StorageServiceFactory {
                 break;
             }
             // 过滤需要忽略迁移的数据
-            List<Entity> newResult = pageResult.stream()
-                .map(entity -> entity.toBeanIgnoreCase(aClass))
-                .map(o -> {
-                    // 兼容大小写
-                    Entity entity = Entity.create(targetModeWrapper.wrap(tableName.value()));
-                    return entity.parseBean(o, false, true);
-                }).peek(entity -> {
-                    if (DbExtConfig.Mode.POSTGRESQL.equals(targetNode)) {
-                        // tinyint类型查出来是数字,需转为bool
-                        boolFieldSet.forEach(fieldName -> {
-                            Object field = entity.get(fieldName);
-                            if (field instanceof Number) {
-                                entity.set(fieldName, BooleanUtil.toBoolean(field.toString()));
-                            }
-                        });
-                    }
-                }).collect(Collectors.toList());
+            List<Entity> newResult = pageResult.stream().map(entity -> entity.toBeanIgnoreCase(aClass)).map(o -> {
+                // 兼容大小写
+                Entity entity = Entity.create(targetModeWrapper.wrap(value));
+                return entity.parseBean(o, false, true);
+            }).peek(entity -> {
+                if (DbExtConfig.Mode.POSTGRESQL.equals(targetNode)) {
+                    // tinyint类型查出来是数字,需转为bool
+                    boolFieldSet.forEach(fieldName -> {
+                        Object field = entity.get(fieldName);
+                        if (field instanceof Number) {
+                            entity.set(fieldName, BooleanUtil.toBoolean(field.toString()));
+                        }
+                    });
+                }
+            }).collect(Collectors.toList());
             if (newResult.isEmpty()) {
                 if (pageResult.isLast()) {
                     // 最后一页
@@ -192,7 +205,7 @@ public class StorageServiceFactory {
             }
 
             // 删除数据
-            Entity deleteWhere = Entity.create(tableName.value());
+            Entity deleteWhere = Entity.create(value);
             deleteWhere.set("id", newResult.stream().map(entity -> entity.getStr("id")).collect(Collectors.toList()));
             db.del(deleteWhere);
         }
@@ -206,7 +219,7 @@ public class StorageServiceFactory {
      * @return sha1 log
      * @author bwcx_jzy
      */
-    public static Set<String> loadExecuteSqlLog() {
+    public Set<String> loadExecuteSqlLog() {
         File localPath = dbLocalPath();
         File file = FileUtil.file(localPath, "execute.init.sql.log");
         if (!FileUtil.isFile(file)) {
@@ -224,14 +237,14 @@ public class StorageServiceFactory {
      * @return 默认本地数据目录下面的 db 目录
      * @author bwcx_jzy
      */
-    public static File dbLocalPath() {
+    public File dbLocalPath() {
         return FileUtil.file(ExtConfigBean.getPath(), DB);
     }
 
     /**
      * 清除执行记录
      */
-    public static void clearExecuteSqlLog() {
+    public void clearExecuteSqlLog() {
         File localPath = dbLocalPath();
         File file = FileUtil.file(localPath, "execute.init.sql.log");
         FileUtil.del(file);
@@ -242,7 +255,7 @@ public class StorageServiceFactory {
      *
      * @author bwcx_jzy
      */
-    public static void saveExecuteSqlLog(Set<String> logs) {
+    public void saveExecuteSqlLog(Set<String> logs) {
         File localPath = dbLocalPath();
         File file = FileUtil.file(localPath, "execute.init.sql.log");
         FileUtil.writeUtf8Lines(logs, file);
@@ -253,7 +266,7 @@ public class StorageServiceFactory {
      *
      * @return 单例的 IStorageService
      */
-    public static IStorageService get() {
+    public IStorageService get() {
         Assert.notNull(mode, I18nMessageUtil.get("i18n.unknown_database_mode.f9e5"));
         return Singleton.get(IStorageService.class.getName(), (CheckedUtil.Func0Rt<IStorageService>) () -> doCreateStorageService(mode));
     }
@@ -265,7 +278,7 @@ public class StorageServiceFactory {
      *
      * @return {@code EngineFactory}
      */
-    private static IStorageService doCreateStorageService(DbExtConfig.Mode mode) {
+    private IStorageService doCreateStorageService(DbExtConfig.Mode mode) {
         final List<IStorageService> storageServiceList = ServiceLoaderUtil.loadList(IStorageService.class);
         if (storageServiceList != null) {
             for (IStorageService storageService : storageServiceList) {

+ 13 - 2
modules/storage-module/storage-module-common/src/main/java/org/dromara/jpom/db/StorageTableFactory.java

@@ -28,6 +28,8 @@ import java.io.BufferedReader;
 import java.io.InputStream;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 /**
  * @author bwcx_jzy
@@ -49,6 +51,9 @@ public class StorageTableFactory {
             CsvReader csvReader = CsvUtil.getReader(csvReadConfig);
             BufferedReader bufferedReader = IoUtil.getUtf8Reader(inputStream);
             List<TableViewData> tableViewData = csvReader.read(bufferedReader, TableViewData.class);
+            tableViewData = tableViewData.stream()
+                .peek(data -> data.setTableName(StorageServiceFactory.getInstance().parseRealTableName(data.getTableName())))
+                .collect(Collectors.toList());
 
             Map<String, List<TableViewData>> map = CollStreamUtil.groupByKey(tableViewData, TableViewData::getTableName);
             StringBuilder stringBuffer = new StringBuilder();
@@ -79,6 +84,9 @@ public class StorageTableFactory {
             CsvReader csvReader = CsvUtil.getReader(csvReadConfig);
             BufferedReader bufferedReader = IoUtil.getUtf8Reader(inputStream);
             List<TableViewAlterData> tableViewData = csvReader.read(bufferedReader, TableViewAlterData.class);
+            tableViewData = tableViewData.stream()
+                .peek(data -> data.setTableName(StorageServiceFactory.getInstance().parseRealTableName(data.getTableName())))
+                .collect(Collectors.toList());
             IStorageSqlBuilderService sqlBuilderService = StorageTableFactory.get();
             return sqlBuilderService.generateAlterTableSql(tableViewData);
 
@@ -94,6 +102,9 @@ public class StorageTableFactory {
             CsvReader csvReader = CsvUtil.getReader(csvReadConfig);
             BufferedReader bufferedReader = IoUtil.getUtf8Reader(inputStream);
             List<TableViewIndexData> tableViewData = csvReader.read(bufferedReader, TableViewIndexData.class);
+            tableViewData = tableViewData.stream()
+                .peek(data -> data.setTableName(StorageServiceFactory.getInstance().parseRealTableName(data.getTableName())))
+                .collect(Collectors.toList());
             IStorageSqlBuilderService sqlBuilderService = StorageTableFactory.get();
             return sqlBuilderService.generateIndexSql(tableViewData);
 
@@ -108,8 +119,8 @@ public class StorageTableFactory {
      * @return 单例的 IStorageSqlBuilderService
      */
     public static IStorageSqlBuilderService get() {
-        Assert.notNull(StorageServiceFactory.getMode(), I18nMessageUtil.get("i18n.unknown_database_mode.f9e5"));
-        return Singleton.get(IStorageSqlBuilderService.class.getName(), (CheckedUtil.Func0Rt<IStorageSqlBuilderService>) () -> doCreateStorageService(StorageServiceFactory.getMode()));
+        Assert.notNull(StorageServiceFactory.getInstance().getMode(), I18nMessageUtil.get("i18n.unknown_database_mode.f9e5"));
+        return Singleton.get(IStorageSqlBuilderService.class.getName(), (CheckedUtil.Func0Rt<IStorageSqlBuilderService>) () -> doCreateStorageService(StorageServiceFactory.getInstance().getMode()));
     }
 
 

+ 18 - 1
modules/storage-module/storage-module-common/src/main/java/org/dromara/jpom/db/TableViewRowData.java

@@ -17,11 +17,28 @@ import lombok.Data;
  */
 @Data
 public class TableViewRowData {
-
+    /**
+     * 字段名
+     */
     private String name;
+    /**
+     * 字段类型
+     */
     private String type;
+    /**
+     * 字段长度
+     */
     private Integer len;
+    /**
+     * 默认值
+     */
     private String defaultValue;
+    /**
+     * 是否非空
+     */
     private Boolean notNull;
+    /**
+     * 字段备注
+     */
     private String comment;
 }

+ 61 - 3
modules/storage-module/storage-module-common/src/main/java/org/dromara/jpom/dialect/DialectUtil.java

@@ -10,18 +10,18 @@
 package org.dromara.jpom.dialect;
 
 import cn.hutool.db.dialect.Dialect;
+import cn.hutool.db.dialect.impl.DmDialect;
 import cn.hutool.db.dialect.impl.H2Dialect;
 import cn.hutool.db.dialect.impl.MysqlDialect;
 import cn.hutool.db.dialect.impl.PostgresqlDialect;
 import cn.hutool.db.sql.Wrapper;
 import cn.hutool.extra.spring.SpringUtil;
-import org.dromara.jpom.common.i18n.I18nMessageUtil;
-import org.dromara.jpom.db.DbExtConfig;
-
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
+import org.dromara.jpom.common.i18n.I18nMessageUtil;
+import org.dromara.jpom.db.DbExtConfig;
 
 /**
  * 数据库方言工具类
@@ -50,6 +50,62 @@ public class DialectUtil {
         return DIALECT_CACHE.computeIfAbsent(DbExtConfig.Mode.MYSQL, key -> new MysqlDialect());
     }
 
+    public static Dialect getDmDialect() {
+        return DIALECT_CACHE.computeIfAbsent(DbExtConfig.Mode.DAMENG, key -> {
+            Set<String> DAMENG_KEYWORDS = Stream.of(
+                "USER", "ORDER", "GROUP", "LEVEL", "MODE", "TABLE", "SELECT", "INDEX",
+                "COLUMN", "VALUES", "WHERE", "FROM", "UPDATE", "DELETE", "INSERT",
+                "CREATE", "ALTER", "DROP", "PRIMARY", "KEY", "VIEW", "TRIGGER", "SEQUENCE",
+                "SESSION", "ONLINE", "PASSWORD", "DEFAULT", "GRANT", "OPTION"
+            ).collect(Collectors.toSet());
+
+            final char DAMENG_QUOTE_CHAR = '"'; // 达梦的引用字符
+
+            Wrapper dmWrapper = new Wrapper(DAMENG_QUOTE_CHAR) {
+
+                @Override
+                public String wrap(String field) {
+                    if (field == null) {
+                        return null;
+                    }
+
+                    // 1. 尝试去除字段本身可能带有的引号
+                    String unWrappedField = field;
+                    if (field.length() >= 2 && field.charAt(0) == DAMENG_QUOTE_CHAR && field.charAt(field.length() - 1) == DAMENG_QUOTE_CHAR) {
+                        unWrappedField = field.substring(1, field.length() - 1);
+                    }
+                    // 2. 将处理后的字段名转换为大写
+                    String upperCaseField = unWrappedField.toUpperCase();
+
+                    // 3. 检查转换后的大写字段名是否为关键字
+                    if (DAMENG_KEYWORDS.contains(upperCaseField)) {
+                        // 如果是关键字,则给这个大写形式的字段名加上双引号
+                        return DAMENG_QUOTE_CHAR + upperCaseField + DAMENG_QUOTE_CHAR;
+                    } else {
+                        // 如果不是关键字,则返回大写形式的字段名(不加引号)
+                        return upperCaseField;
+                    }
+                }
+
+                @Override
+                public String unWrap(String field) {
+                    if (field == null) {
+                        return null;
+                    }
+                    // 标准的去除引号逻辑
+                    if (field.length() >= 2 && field.charAt(0) == DAMENG_QUOTE_CHAR && field.charAt(field.length() - 1) == DAMENG_QUOTE_CHAR) {
+                        return field.substring(1, field.length() - 1);
+                    }
+                    return field;
+                }
+            };
+
+            DmDialect dialect = new DmDialect();
+            dialect.setWrapper(dmWrapper);
+            return dialect;
+        });
+    }
+
     /**
      * 获取自定义postgresql数据库的方言
      *
@@ -93,6 +149,8 @@ public class DialectUtil {
                 return getMySqlDialect();
             case POSTGRESQL:
                 return getPostgresqlDialect();
+            case DAMENG:
+                return getDmDialect();
             default:
                 throw new IllegalArgumentException(I18nMessageUtil.get("i18n.unknown_database_dialect_type.951b") + mode);
         }

+ 9 - 0
modules/storage-module/storage-module-dameng/README.md

@@ -0,0 +1,9 @@
+# dameng
+
+https://juejin.cn/post/7318446303536594953
+
+https://eco.dameng.com/community/training/afa88aa3975bf73c08a7d9058310e511
+
+```shell
+docker run -d -p 5236:5236 --restart=always --name dameng --privileged=true -e PAGE_SIZE=16 -e LD_LIBRARY_PATH=/opt/dmdbms/bin -e INSTANCE_NAME=dm8db xuxuclassmate/dameng
+```

+ 53 - 0
modules/storage-module/storage-module-dameng/pom.xml

@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Copyright (c) 2019 Of Him Code Technology Studio
+    Jpom is licensed under Mulan PSL v2.
+    You can use this software according to the terms and conditions of the Mulan PSL v2.
+    You may obtain a copy of Mulan PSL v2 at:
+    			http://license.coscl.org.cn/MulanPSL2
+    THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+    See the Mulan PSL v2 for more details.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.dromara.jpom.storage-module</groupId>
+        <artifactId>jpom-storage-module-parent</artifactId>
+        <version>2.11.12.1</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>storage-module-dameng</artifactId>
+
+    <properties>
+        <maven.compiler.source>8</maven.compiler.source>
+        <maven.compiler.target>8</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.dromara.jpom.storage-module</groupId>
+            <artifactId>storage-module-common</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.dameng</groupId>
+            <artifactId>Dm8JdbcDriver18</artifactId>
+            <version>8.1.1.49</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara.jpom</groupId>
+            <artifactId>common</artifactId>
+            <scope>provided</scope>
+            <version>${project.version}</version>
+        </dependency>
+    </dependencies>
+
+</project>

+ 96 - 0
modules/storage-module/storage-module-dameng/src/main/java/org/dromara/jpom/storage/DamengStorageServiceImpl.java

@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2019 Of Him Code Technology Studio
+ * Jpom is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ * 			http://license.coscl.org.cn/MulanPSL2
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+package org.dromara.jpom.storage;
+
+import cn.hutool.core.lang.Opt;
+import cn.hutool.db.ds.DSFactory;
+import cn.hutool.setting.Setting;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.jpom.common.i18n.I18nMessageUtil;
+import org.dromara.jpom.db.DbExtConfig;
+import org.dromara.jpom.db.IStorageService;
+import org.dromara.jpom.system.JpomRuntimeException;
+import org.springframework.util.Assert;
+
+/**
+ * @author bwcx_jzy
+ * @since 2025/5/8
+ */
+@Slf4j
+public class DamengStorageServiceImpl implements IStorageService {
+
+    private String dbUrl;
+    private DSFactory dsFactory;
+
+    @Override
+    public String dbUrl() {
+        Assert.hasText(this.dbUrl, I18nMessageUtil.get("i18n.database_not_initialized.e5e7"));
+        return dbUrl;
+    }
+
+    @Override
+    public int getFetchSize() {
+        return Integer.MIN_VALUE;
+    }
+
+    @Override
+    public DbExtConfig.Mode mode() {
+        return DbExtConfig.Mode.DAMENG;
+    }
+
+    @Override
+    public DSFactory init(DbExtConfig dbExtConfig) {
+        Assert.isNull(this.dsFactory, I18nMessageUtil.get("i18n.do_not_reinitialize_database.9bb5"));
+        Assert.hasText(dbExtConfig.getUrl(), I18nMessageUtil.get("i18n.database_connection_not_configured.c80e"));
+        Setting setting = dbExtConfig.toSetting();
+        this.dsFactory = DSFactory.create(setting);
+        this.dbUrl = dbExtConfig.getUrl();
+        return this.dsFactory;
+    }
+
+    @Override
+    public DSFactory create(DbExtConfig dbExtConfig, String url, String user, String pass) {
+        Setting setting = this.createSetting(dbExtConfig, url, user, pass);
+        return DSFactory.create(setting);
+    }
+
+    @Override
+    public Setting createSetting(DbExtConfig dbExtConfig, String url, String user, String pass) {
+        String url2 = Opt.ofBlankAble(url).orElse(dbExtConfig.getUrl());
+        String user2 = Opt.ofBlankAble(user).orElse(dbExtConfig.getUserName());
+        String pass2 = Opt.ofBlankAble(pass).orElse(dbExtConfig.getUserPwd());
+        Setting setting = dbExtConfig.toSetting();
+        setting.set("user", user2);
+        setting.set("pass", pass2);
+        setting.set("url", url2);
+        return setting;
+    }
+
+    public DSFactory getDsFactory() {
+        Assert.notNull(this.dsFactory, I18nMessageUtil.get("i18n.database_not_initialized.e5e7"));
+        return dsFactory;
+    }
+
+
+    @Override
+    public JpomRuntimeException warpException(Exception e) {
+        return new JpomRuntimeException(I18nMessageUtil.get("i18n.database_exception.4894"), e);
+    }
+
+
+    @Override
+    public void close() throws Exception {
+        log.info("dameng db destroy");
+        if (this.dsFactory != null) {
+            dsFactory.destroy();
+            this.dsFactory = null;
+        }
+    }
+}

+ 298 - 0
modules/storage-module/storage-module-dameng/src/main/java/org/dromara/jpom/storage/DamengTableBuilderImpl.java

@@ -0,0 +1,298 @@
+/*
+ * Copyright (c) 2019 Of Him Code Technology Studio
+ * Jpom is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ * 			http://license.coscl.org.cn/MulanPSL2
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+package org.dromara.jpom.storage;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import org.dromara.jpom.common.i18n.I18nMessageUtil;
+import org.dromara.jpom.db.*;
+import org.springframework.util.Assert;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author bwcx_jzy
+ * @since 2025/5/8
+ */
+public class DamengTableBuilderImpl implements IStorageSqlBuilderService {
+
+    @Override
+    public DbExtConfig.Mode mode() {
+        return DbExtConfig.Mode.DAMENG;
+    }
+
+    /**
+     * 为标识符添加引号(达梦数据库使用双引号)。
+     * @param identifier 标识符字符串
+     * @return 加引号后的标识符,如果原标识符为空则返回原样
+     */
+    private String quoteIdentifier(String identifier) {
+        if (StrUtil.isEmpty(identifier)) {
+            return identifier;
+        }
+        // 达梦数据库使用双引号来界定标识符,以保留大小写或允许特殊字符
+        return "\"" + identifier + "\"";
+    }
+
+    /**
+     * 为一组标识符添加引号。
+     * @param identifiers 标识符列表
+     * @return 加引号后的标识符列表
+     */
+    private List<String> quoteIdentifiers(List<String> identifiers) {
+        if (CollUtil.isEmpty(identifiers)) {
+            return identifiers;
+        }
+        return identifiers.stream().map(this::quoteIdentifier).collect(Collectors.toList());
+    }
+
+    @Override
+    public String generateIndexSql(List<TableViewIndexData> row) {
+        StringBuilder stringBuilder = new StringBuilder();
+        for (TableViewIndexData viewIndexData : row) {
+            String indexType = viewIndexData.getIndexType();
+            // 用于 CREATE INDEX 语句的表名和索引名需要加引号
+            String tableNameForCreateIndex = quoteIdentifier(viewIndexData.getTableName());
+            String indexNameForCreateIndex = quoteIdentifier(viewIndexData.getName());
+
+            String field = viewIndexData.getField();
+            List<String> fields = StrUtil.splitTrim(field, "+");
+            Assert.notEmpty(fields, I18nMessageUtil.get("i18n.index_field_not_configured.96d9"));
+            String columnsToIdx = CollUtil.join(quoteIdentifiers(fields), StrUtil.COMMA);
+
+            // 调用存储过程。假设存储过程内部处理未加引号的名称,或根据数据库设置处理大小写。
+            // 如果存储过程期望接收已加引号的名称,则应在此处传递 tableNameForCreateIndex 和 indexNameForCreateIndex。
+            // 通常,传递给存储过程的字符串参数是原始名称。
+            stringBuilder.append("CALL drop_index_if_exists('").append(viewIndexData.getTableName()).append("','").append(viewIndexData.getName()).append("')").append(";").append(StrUtil.LF);
+            stringBuilder.append(this.delimiter()).append(StrUtil.LF);
+
+            switch (indexType) {
+                case "ADD-UNIQUE":
+                    // 达梦: CREATE UNIQUE INDEX "索引名" ON "表名" ("列1", "列2", ...);
+                    stringBuilder.append("CREATE UNIQUE INDEX ").append(indexNameForCreateIndex).append(" ON ").append(tableNameForCreateIndex).append(" (").append(columnsToIdx).append(")");
+                    break;
+                case "ADD":
+                    // 达梦: CREATE INDEX "索引名" ON "表名" ("列1", "列2", ...);
+                    stringBuilder.append("CREATE INDEX ").append(indexNameForCreateIndex).append(" ON ").append(tableNameForCreateIndex).append(" (").append(columnsToIdx).append(")");
+                    break;
+                default:
+                    throw new IllegalArgumentException(I18nMessageUtil.get("i18n.unsupported_type_with_colon2.7de2") + indexType);
+            }
+            stringBuilder.append(";").append(StrUtil.LF);
+            stringBuilder.append(this.delimiter()).append(StrUtil.LF);
+        }
+        return stringBuilder.toString();
+    }
+
+    @Override
+    public String generateAlterTableSql(List<TableViewAlterData> row) {
+        StringBuilder stringBuilder = new StringBuilder();
+        for (TableViewAlterData viewAlterData : row) {
+            String alterType = viewAlterData.getAlterType();
+            // DDL语句中的表名需要加引号
+            String tableNameForDdl = quoteIdentifier(viewAlterData.getTableName());
+            // 从 viewAlterData 获取的原始列名 (未加引号)
+            String columnNameUnquoted = viewAlterData.getName();
+
+            switch (alterType) {
+                case "DROP":
+                    // 调用存储过程,传递原始未加引号的名称
+                    stringBuilder.append("CALL drop_column_if_exists('").append(viewAlterData.getTableName()).append("', '").append(columnNameUnquoted).append("')");
+                    break;
+                case "ADD":
+                    // generateColumnSql 方法会处理其自身列名的引号。
+                    // encode=true 用于在字符串字面量内转义, includeComment=true
+                    String columnSqlForAdd = this.generateColumnSql(viewAlterData, true, true);
+                    // 调用存储过程,传递原始未加引号的表名/列名
+                    stringBuilder.append("CALL add_column_if_not_exists('").append(viewAlterData.getTableName()).append("','").append(columnNameUnquoted).append("','").append(columnSqlForAdd).append("')");
+                    break;
+                case "ALTER":
+                    // 达梦: ALTER TABLE "表名" MODIFY ("列定义不含注释");
+                    // 列注释的更新在达梦中不通过 MODIFY 内联处理。
+                    // 调用 generateColumnSql 时设置 includeComment=false
+                    String columnSqlForModify = this.generateColumnSql(viewAlterData, false, false);
+                    stringBuilder.append("ALTER TABLE ").append(tableNameForDdl).append(" MODIFY (").append(columnSqlForModify).append(")");
+                    // 如果需要更新注释,需要一个独立的 "COMMENT ON COLUMN" 语句。
+                    // 当前框架可能不支持为一个 "ALTER" 操作生成两条语句。
+                    // 因此,通过此构建器的 ALTER MODIFY 操作实际上会跳过注释的更新。
+                    break;
+                case "DROP-TABLE":
+                    stringBuilder.append("DROP TABLE IF EXISTS ").append(tableNameForDdl);
+                    break;
+                default:
+                    throw new IllegalArgumentException(I18nMessageUtil.get("i18n.unsupported_type_with_colon2.7de2") + alterType);
+            }
+            stringBuilder.append(";").append(StrUtil.LF); // SQL语句结束符
+            stringBuilder.append(this.delimiter()).append(StrUtil.LF); // 追加自定义分隔符
+        }
+        return stringBuilder.toString();
+    }
+
+    /**
+     * 生成创建表的SQL语句。
+     * 如果提供了表描述(desc),则会额外生成一条 COMMENT ON TABLE 语句。
+     * @param name 表名
+     * @param desc 表描述
+     * @param row  列定义列表
+     * @return 生成的SQL字符串,可能包含多条语句(CREATE TABLE 和 COMMENT ON TABLE)
+     */
+    @Override
+    public String generateTableSql(String name, String desc, List<TableViewData> row) {
+        StringBuilder stringBuilder = new StringBuilder();
+        String tableNameQuoted = quoteIdentifier(name); // 表名加引号
+        stringBuilder.append("CREATE TABLE IF NOT EXISTS ").append(tableNameQuoted).append(StrUtil.LF);
+        stringBuilder.append("(").append(StrUtil.LF);
+
+        boolean hasColumns = false;
+        if (CollUtil.isNotEmpty(row)) {
+            List<String> columnSqlList = row.stream()
+                .map(tableViewData -> StrUtil.TAB + this.generateColumnSql(tableViewData, false, true)) // encode=false, includeComment=true (列内联注释)
+                .collect(Collectors.toList());
+
+            if (!columnSqlList.isEmpty()) {
+                stringBuilder.append(String.join("," + StrUtil.LF, columnSqlList));
+                hasColumns = true;
+            }
+        }
+
+        List<String> primaryKeyNames = row.stream()
+            .filter(tableViewData -> tableViewData.getPrimaryKey() != null && tableViewData.getPrimaryKey())
+            .map(TableViewRowData::getName)
+            .filter(StrUtil::isNotEmpty)
+            .collect(Collectors.toList());
+
+        if (CollUtil.isNotEmpty(primaryKeyNames)) {
+            if (hasColumns) { // 如果前面有列定义,则在主键前加逗号和换行
+                stringBuilder.append(",").append(StrUtil.LF);
+            } else { // 如果没有其他列,主键是第一项,确保换行
+                stringBuilder.append(StrUtil.LF);
+            }
+            stringBuilder.append(StrUtil.TAB).append("PRIMARY KEY (").append(CollUtil.join(quoteIdentifiers(primaryKeyNames), StrUtil.COMMA)).append(")");
+        }
+
+        stringBuilder.append(StrUtil.LF).append(")"); // 结束表定义括号
+        stringBuilder.append(this.delimiter()); // CREATE TABLE 语句结束
+
+        // 如果有表描述,则生成独立的 COMMENT ON TABLE 语句
+        if (StrUtil.isNotEmpty(desc)) {
+            stringBuilder.append(StrUtil.LF); // 换行开始新的语句
+            // 注意:这里不主动添加 this.delimiter(),因为这个方法返回的是一个“逻辑块”的SQL。
+            // 如果调用者(如InitDb)期望在多个逻辑块之间添加分隔符,它会在循环外添加。
+            // 如果InitDb把这个返回的字符串直接执行,分号已经足够分隔语句了。
+            stringBuilder.append("COMMENT ON TABLE ").append(tableNameQuoted).append(" IS '").append(StrUtil.replace(desc, "'", "''")).append("';"); // 转义描述中的单引号
+        }
+        return stringBuilder.toString();
+    }
+
+    @Override
+    public String generateColumnSql(TableViewRowData tableViewRowData) {
+        // 默认调用: encode=false (不为存储过程参数转义), includeComment=true (包含列注释)
+        return generateColumnSql(tableViewRowData, false, true);
+    }
+
+    /**
+     * 生成单个列定义的SQL片段。
+     * @param tableViewRowData 列的元数据
+     * @param encode 是否为用作SQL字符串字面量内部值而转义特殊字符 (例如,传递给存储过程的参数)
+     * @param includeComment 是否在列定义中包含 COMMENT 子句 (用于 CREATE TABLE)
+     * @return 生成的列定义SQL字符串
+     */
+    private String generateColumnSql(TableViewRowData tableViewRowData, boolean encode, boolean includeComment) {
+        StringBuilder stringBuilder = new StringBuilder();
+        stringBuilder.append(quoteIdentifier(tableViewRowData.getName())).append(StrUtil.SPACE); // 列名加引号
+
+        String type = tableViewRowData.getType();
+        Assert.hasText(type, I18nMessageUtil.get("i18n.data_type_not_configured_correctly.bf16"));
+        String upperType = type.toUpperCase(); // 使用本地变量存储大写类型
+
+        // 数据类型映射
+        switch (upperType) {
+            case "LONG":
+                stringBuilder.append("BIGINT").append(StrUtil.SPACE);
+                break;
+            case "STRING":
+                stringBuilder.append("VARCHAR(").append(ObjectUtil.defaultIfNull(tableViewRowData.getLen(), 255)).append(")").append(StrUtil.SPACE);
+                break;
+            case "TEXT":
+                stringBuilder.append("CLOB").append(StrUtil.SPACE); // 达梦使用 CLOB 存储大文本
+                break;
+            case "INTEGER":
+                stringBuilder.append("INT").append(StrUtil.SPACE);
+                break;
+            case "TINYINT":
+                stringBuilder.append("TINYINT").append(StrUtil.SPACE); // 达梦 TINYINT 是有符号的 (-128 到 127)
+                break;
+            case "FLOAT":
+                stringBuilder.append("REAL").append(StrUtil.SPACE); // 达梦 REAL 对应单精度浮点数
+                break;
+            case "DOUBLE":
+                stringBuilder.append("DOUBLE PRECISION").append(StrUtil.SPACE); // 达梦 DOUBLE PRECISION 对应双精度浮点数
+                break;
+            default:
+                throw new IllegalArgumentException(I18nMessageUtil.get("i18n.data_type_not_supported.fd03") + type);
+        }
+
+        // 处理 NOT NULL 约束
+        Boolean notNull = tableViewRowData.getNotNull();
+        if (notNull != null && notNull) {
+            stringBuilder.append("NOT NULL").append(StrUtil.SPACE);
+        } else {
+            // 显式添加 NULL,更清晰,尽管通常省略 NOT NULL 时默认为 NULL
+            stringBuilder.append("NULL").append(StrUtil.SPACE);
+        }
+
+        // 处理默认值
+        String defaultValue = tableViewRowData.getDefaultValue();
+        if (StrUtil.isNotEmpty(defaultValue)) {
+            stringBuilder.append("DEFAULT ");
+            // 仅对字符串/文本类类型的默认值加引号
+            if (upperType.equals("STRING") || upperType.equals("TEXT")) { // TEXT 已映射为 CLOB
+                stringBuilder.append("'").append(StrUtil.replace(defaultValue, "'", "''")).append("'"); // 达梦转义单引号用两个单引号
+            } else if (upperType.equals("LONG") || upperType.equals("INTEGER") || upperType.equals("TINYINT") ||
+                upperType.equals("FLOAT") || upperType.equals("DOUBLE")) {
+                stringBuilder.append(defaultValue); // 数字类型默认值不加引号
+            } else {
+                // 其他未知类型,默认加引号。如果需要处理 CURRENT_TIMESTAMP 等关键字,需特殊处理。
+                stringBuilder.append("'").append(StrUtil.replace(defaultValue, "'", "''")).append("'");
+            }
+            stringBuilder.append(StrUtil.SPACE);
+        }
+
+        // 处理列注释 (如果允许包含) - 这个注释是列内联的,错误信息指出是表尾的COMMENT有问题
+        if (includeComment && StrUtil.isNotEmpty(tableViewRowData.getComment())) {
+            // 达梦在 CREATE TABLE 时支持内联的列注释
+            stringBuilder.append("COMMENT '").append(StrUtil.replace(tableViewRowData.getComment(), "'", "''")).append("'"); // 转义注释中的单引号
+        }
+
+        // 在进行编码或长度检查前,移除末尾可能存在的空格
+        String columnSql = StrUtil.trimEnd(stringBuilder.toString());
+
+        if (encode) {
+            // 为用作SQL字符串字面量内部的值而转义 (例如,作为 add_column_if_not_exists 过程的参数)
+            // 达梦转义单引号使用两个单引号
+            columnSql = StrUtil.replace(columnSql, "'", "''");
+        }
+
+        // 对生成的列定义SQL的长度进行断言,确保不超过存储过程参数的合理限制
+        // 存储过程 'add_column_if_not_exists' 的 columninfo 参数可能是 VARCHAR(200)
+        // 此限制应略小于存储过程参数的大小。
+        int length = StrUtil.length(columnSql);
+        Assert.state(length <= 190, I18nMessageUtil.get("i18n.sql_statement_too_long.38d6") +
+            " 生成的列SQL定义长度为 " + length + " 字符。用于存储过程参数时最大约为190。");
+        return columnSql;
+    }
+
+    @Override
+    public String delimiter() {
+        return "-- DAMENG DELIMITER";
+    }
+}

+ 1 - 0
modules/storage-module/storage-module-dameng/src/main/resources/META-INF/services/org.dromara.jpom.db.IStorageService

@@ -0,0 +1 @@
+org.dromara.jpom.storage.DamengStorageServiceImpl

+ 1 - 0
modules/storage-module/storage-module-dameng/src/main/resources/META-INF/services/org.dromara.jpom.db.IStorageSqlBuilderService

@@ -0,0 +1 @@
+org.dromara.jpom.storage.DamengTableBuilderImpl

+ 149 - 0
modules/storage-module/storage-module-dameng/src/main/resources/sql-view/execute.dameng.v1.0.sql

@@ -0,0 +1,149 @@
+--
+-- Copyright (c) 2019 Of Him Code Technology Studio
+-- Jpom is licensed under Mulan PSL v2.
+-- You can use this software according to the terms and conditions of the Mulan PSL v2.
+-- You may obtain a copy of Mulan PSL v2 at:
+-- 			http://license.coscl.org.cn/MulanPSL2
+-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+-- See the Mulan PSL v2 for more details.
+--
+
+DROP FUNCTION IF EXISTS column_exists;
+
+-- DAMENG DELIMITER
+
+CREATE OR REPLACE FUNCTION column_exists(
+    tname VARCHAR2,
+    cname VARCHAR2
+)
+RETURN BOOLEAN
+IS
+    v_count INT;
+BEGIN
+SELECT COUNT(*) INTO v_count
+FROM DBA_TAB_COLUMNS  -- 或者使用 USER_TAB_COLUMNS,根据权限选择
+WHERE OWNER = CURRENT_USER -- 当前用户(在达梦中可能需要使用 OWNER)
+  AND TABLE_NAME = UPPER(tname)  -- 达梦通常将元数据以大写存储
+  AND COLUMN_NAME = UPPER(cname);  -- 列名也使用大写处理
+
+RETURN (v_count > 0);  -- 如果找到列,返回 TRUE,否则返回 FALSE
+END
+
+
+-- DAMENG DELIMITER
+
+DROP PROCEDURE IF EXISTS drop_column_if_exists;
+
+-- DAMENG DELIMITER
+
+CREATE OR REPLACE PROCEDURE drop_column_if_exists(
+    tname IN VARCHAR2,
+    cname IN VARCHAR2
+)
+IS
+    v_count INT;
+drop_query VARCHAR2(4000);
+BEGIN
+-- 检查列是否存在
+SELECT COUNT(*) INTO v_count
+FROM DBA_TAB_COLUMNS  -- 使用适合的视图,如 USER_TAB_COLUMNS
+WHERE OWNER = CURRENT_USER
+  AND TABLE_NAME = UPPER(tname)
+  AND COLUMN_NAME = UPPER(cname);
+
+IF v_count > 0 THEN
+        -- 构造动态 SQL 删除列
+        drop_query := 'ALTER TABLE "' || UPPER(tname) || '" DROP COLUMN "' || UPPER(cname) || '"';
+
+        -- 执行动态 SQL
+EXECUTE IMMEDIATE drop_query;
+END IF;
+END;
+
+
+-- DAMENG DELIMITER
+
+DROP PROCEDURE IF EXISTS add_column_if_not_exists;
+
+-- DAMENG DELIMITER
+
+CREATE OR REPLACE PROCEDURE add_column_if_not_exists(
+    tname IN VARCHAR2,
+    cname IN VARCHAR2,
+    columninfo IN VARCHAR2
+)
+IS
+    v_count INT;
+add_column_sql VARCHAR2(4000);
+comment_sql VARCHAR2(4000);
+def_part VARCHAR2(3000);
+comment_part VARCHAR2(1000);
+comment_pos INT;
+BEGIN
+-- 判断列是否存在(保留原始大小写)
+SELECT COUNT(*) INTO v_count
+FROM USER_TAB_COLUMNS
+WHERE TABLE_NAME = UPPER(tname)
+  AND COLUMN_NAME = cname;  -- 注意:不要用 UPPER(cname),否则误判
+
+IF v_count = 0 THEN
+        -- 解析 COMMENT 部分
+        comment_pos := INSTR(UPPER(columninfo), ' COMMENT ');
+IF comment_pos > 0 THEN
+            def_part := TRIM(SUBSTR(columninfo, 1, comment_pos - 1));
+comment_part := TRIM(REPLACE(SUBSTR(columninfo, comment_pos + 8), '''', ''));
+ELSE
+            def_part := columninfo;
+comment_part := NULL;
+END IF;
+
+        -- 添加列
+add_column_sql := 'ALTER TABLE "' || UPPER(tname) || '" ADD ' || def_part;
+EXECUTE IMMEDIATE add_column_sql;
+
+-- 添加注释(如果有)
+IF comment_part IS NOT NULL THEN
+            comment_sql := 'COMMENT ON COLUMN "' || UPPER(tname) || '"."' || cname || '" IS ''' || comment_part || '''';
+EXECUTE IMMEDIATE comment_sql;
+END IF;
+END IF;
+END;
+
+
+
+-- DAMENG DELIMITER
+
+DROP PROCEDURE IF EXISTS drop_index_if_exists;
+
+-- DAMENG DELIMITER
+
+CREATE OR REPLACE PROCEDURE drop_index_if_exists(
+    p_tablename IN VARCHAR2,
+    p_idxname IN VARCHAR2
+)
+IS
+    v_count INT;
+drop_index_sql VARCHAR2(4000);
+BEGIN
+-- 步骤 1: 查找当前用户(模式)拥有的,并且是建立在指定表上的该索引
+-- USER_INDEXES 视图只显示当前用户拥有的索引
+SELECT COUNT(*)
+INTO v_count
+FROM USER_INDEXES
+WHERE TABLE_NAME = UPPER(p_tablename)   -- 确保索引是属于 p_tablename 指定的表
+  AND INDEX_NAME = UPPER(p_idxname);    -- 匹配索引名 (转换为大写以兼容不区分大小写的场景)
+
+-- 步骤 2: 如果在当前模式中找到了该索引
+IF v_count > 0 THEN
+        -- 构造 DROP INDEX SQL 语句
+        -- 对于当前模式的索引,DROP INDEX 语句不需要指定模式名(用户名)
+        -- 直接使用索引名即可,加上双引号以处理特殊字符或大小写敏感的索引名(如果需要)
+        drop_index_sql := 'DROP INDEX "' || UPPER(p_idxname) || '"';
+EXECUTE IMMEDIATE drop_index_sql;
+END IF;
+
+EXCEPTION
+    WHEN OTHERS THEN
+        -- 此处选择重新抛出异常,以便调用者能感知到非“索引不存在”类型的错误。
+        RAISE;
+END drop_index_if_exists;

+ 1 - 1
modules/storage-module/storage-module-h2/pom.xml

@@ -17,7 +17,7 @@
     <parent>
         <groupId>org.dromara.jpom.storage-module</groupId>
         <artifactId>jpom-storage-module-parent</artifactId>
-        <version>2.11.12</version>
+        <version>2.11.12.1</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
 

+ 1 - 1
modules/storage-module/storage-module-h2/src/main/java/org/dromara/jpom/plugin/DefaultDbH2PluginImpl.java

@@ -59,7 +59,7 @@ public class DefaultDbH2PluginImpl implements IDefaultPlugin {
             DataSource dataSource = (DataSource) parameter.get("dataSource");
             if (dataSource == null) {
                 // 加载数据源
-                dataSource = StorageServiceFactory.get().getDsFactory().getDataSource();
+                dataSource = StorageServiceFactory.getInstance().get().getDsFactory().getDataSource();
             }
             this.restoreBackupSql(backupSqlPath, dataSource);
         } else if (StrUtil.equals("recoverToSql", method)) {

+ 6 - 6
modules/storage-module/storage-module-h2/src/main/java/org/dromara/jpom/storage/H2StorageServiceImpl.java

@@ -146,7 +146,7 @@ public class H2StorageServiceImpl implements IStorageService {
      * @return jdbc
      */
     public String getDefaultDbUrl(DbExtConfig dbExtConfig) {
-        File file = FileUtil.file(StorageServiceFactory.dbLocalPath(), this.getDbName());
+        File file = FileUtil.file(StorageServiceFactory.getInstance().dbLocalPath(), this.getDbName());
         String path = FileUtil.getAbsolutePath(file);
         return StrUtil.format("jdbc:h2:{};CACHE_SIZE={};MODE=MYSQL;LOCK_TIMEOUT=10000", path, dbExtConfig.getCacheSize().toKilobytes());
     }
@@ -181,7 +181,7 @@ public class H2StorageServiceImpl implements IStorageService {
      * 恢复数据库
      */
     public File recoverDb() throws Exception {
-        File dbLocalPathFile = StorageServiceFactory.dbLocalPath();
+        File dbLocalPathFile = StorageServiceFactory.getInstance().dbLocalPath();
         if (!FileUtil.exist(dbLocalPathFile)) {
             return null;
         }
@@ -195,14 +195,14 @@ public class H2StorageServiceImpl implements IStorageService {
         map.put("recoverBackup", recoverBackup);
         File backupSql = (File) plugin.execute("recoverToSql", map);
         // 清空记录
-        StorageServiceFactory.clearExecuteSqlLog();
+        StorageServiceFactory.getInstance().clearExecuteSqlLog();
         // 记录恢复的 sql
         return backupSql;
     }
 
     @Override
     public boolean hasDbData() throws Exception {
-        File dbLocalPathFile = StorageServiceFactory.dbLocalPath();
+        File dbLocalPathFile = StorageServiceFactory.getInstance().dbLocalPath();
         if (!FileUtil.exist(dbLocalPathFile)) {
             return false;
         }
@@ -219,7 +219,7 @@ public class H2StorageServiceImpl implements IStorageService {
      */
     @Override
     public String deleteDbFiles() throws Exception {
-        File dbLocalPathFile = StorageServiceFactory.dbLocalPath();
+        File dbLocalPathFile = StorageServiceFactory.getInstance().dbLocalPath();
         if (!FileUtil.exist(dbLocalPathFile)) {
             return null;
         }
@@ -232,7 +232,7 @@ public class H2StorageServiceImpl implements IStorageService {
         map.put("backupPath", deleteBackup);
         plugin.execute("deleteDbFiles", map);
         // 清空记录
-        StorageServiceFactory.clearExecuteSqlLog();
+        StorageServiceFactory.getInstance().clearExecuteSqlLog();
         return FileUtil.getAbsolutePath(deleteBackup);
     }
 

+ 1 - 1
modules/storage-module/storage-module-mariadb/pom.xml

@@ -17,7 +17,7 @@
     <parent>
         <groupId>org.dromara.jpom.storage-module</groupId>
         <artifactId>jpom-storage-module-parent</artifactId>
-        <version>2.11.12</version>
+        <version>2.11.12.1</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
 

+ 1 - 1
modules/storage-module/storage-module-mysql/pom.xml

@@ -17,7 +17,7 @@
     <parent>
         <groupId>org.dromara.jpom.storage-module</groupId>
         <artifactId>jpom-storage-module-parent</artifactId>
-        <version>2.11.12</version>
+        <version>2.11.12.1</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
 

+ 1 - 1
modules/storage-module/storage-module-postgresql/pom.xml

@@ -16,7 +16,7 @@
     <parent>
         <artifactId>jpom-storage-module-parent</artifactId>
         <groupId>org.dromara.jpom.storage-module</groupId>
-        <version>2.11.12</version>
+        <version>2.11.12.1</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 

+ 1 - 1
modules/storage-module/storage-module-postgresql/src/main/java/org/dromara/jpom/storage/PostgresqlTableBuilderImpl.java

@@ -41,7 +41,7 @@ public class PostgresqlTableBuilderImpl implements IStorageSqlBuilderService {
         modelClassSet.forEach(modelClass -> {
             TableName annotation = modelClass.getAnnotation(TableName.class);
             // 统一处理成小写,model也应该不会出现转为小写后重名的field
-            String tableName = annotation.value().toLowerCase();
+            String tableName = StorageServiceFactory.getInstance().parseRealTableName(annotation).toLowerCase();
             Field[] boolFieldArr = ReflectUtil.getFields(modelClass, field -> Boolean.class.equals(field.getType()) || boolean.class.equals(field.getType()));
             Set<String> nameSet = Arrays.stream(boolFieldArr)
                 .map(field -> field.getName().toLowerCase())

+ 1 - 1
modules/sub-plugin/docker-cli/pom.xml

@@ -16,7 +16,7 @@
     <parent>
         <artifactId>jpom-plugins-parent</artifactId>
         <groupId>org.dromara.jpom.plugins</groupId>
-        <version>2.11.12</version>
+        <version>2.11.12.1</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <modelVersion>4.0.0</modelVersion>

+ 4 - 0
modules/sub-plugin/docker-cli/src/main/java/org/dromara/jpom/DefaultDockerPluginImpl.java

@@ -374,6 +374,7 @@ public class DefaultDockerPluginImpl implements IDockerConfigPlugin {
         File baseDirectory = (File) parameter.get("baseDirectory");
         String tags = (String) parameter.get("tags");
         String buildArgs = (String) parameter.get("buildArgs");
+        String networkMode = (String) parameter.get("networkMode");
         Object pull = parameter.get("pull");
         Object noCache = parameter.get("noCache");
         String labels = (String) parameter.get("labels");
@@ -389,6 +390,9 @@ public class DefaultDockerPluginImpl implements IDockerConfigPlugin {
                 .withDockerfile(dockerfile)
                 .withBuildAuthConfigs(authConfigurations)
                 .withTags(CollUtil.newHashSet(StrUtil.splitTrim(tags, StrUtil.COMMA)));
+            if (StrUtil.isNotEmpty(networkMode)){
+                buildImageCmd.withNetworkMode(networkMode);
+            }
             // 添加构建参数
             UrlQuery query = UrlQuery.of(buildArgs, CharsetUtil.CHARSET_UTF_8);
             query.getQueryMap()

+ 1 - 1
modules/sub-plugin/email/pom.xml

@@ -16,7 +16,7 @@
     <parent>
         <artifactId>jpom-plugins-parent</artifactId>
         <groupId>org.dromara.jpom.plugins</groupId>
-        <version>2.11.12</version>
+        <version>2.11.12.1</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <modelVersion>4.0.0</modelVersion>

+ 1 - 1
modules/sub-plugin/encrypt/pom.xml

@@ -16,7 +16,7 @@
     <parent>
         <artifactId>jpom-plugins-parent</artifactId>
         <groupId>org.dromara.jpom.plugins</groupId>
-        <version>2.11.12</version>
+        <version>2.11.12.1</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <modelVersion>4.0.0</modelVersion>

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio