list = ListUtils.newArrayList(mySftpHosts);
+ String fileName = "主机模板.xlsx";
+ try(ExcelExport ee = new ExcelExport("主机", MySftpHosts.class, Type.IMPORT)){
+ ee.setDataList(list).write(response, fileName);
+ }
+ }
+
+ /**
+ * 导入数据
+ */
+ @ResponseBody
+ @RequiresPermissions("biz:mySftpHosts:edit")
+ @PostMapping(value = "importData")
+ public String importData(MultipartFile file) {
+ try {
+ String message = mySftpHostsService.importData(file);
+ return renderResult(Global.TRUE, "posfull:"+message);
+ } catch (Exception ex) {
+ return renderResult(Global.FALSE, "posfull:"+ex.getMessage());
+ }
+ }
+
+ /**
+ * 删除数据
+ */
+ @RequiresPermissions("biz:mySftpHosts:edit")
+ @RequestMapping(value = "delete")
+ @ResponseBody
+ public String delete(MySftpHosts mySftpHosts) {
+ mySftpHostsService.delete(mySftpHosts);
+ return renderResult(Global.TRUE, text("删除主机成功!"));
+ }
+
+}
\ No newline at end of file
diff --git a/web-api/src/main/java/com/jeesite/modules/utils/DockerUtil.java b/web-api/src/main/java/com/jeesite/modules/utils/DockerUtil.java
new file mode 100644
index 0000000..a34e0a2
--- /dev/null
+++ b/web-api/src/main/java/com/jeesite/modules/utils/DockerUtil.java
@@ -0,0 +1,176 @@
+package com.jeesite.modules.utils;
+
+import com.jcraft.jsch.*;
+import com.jeesite.modules.apps.Module.ContainerInfo;
+import com.jeesite.modules.apps.Module.DockerResult;
+import com.jeesite.modules.biz.entity.MySftpAccounts;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Docker 容器管理工具
+ *
+ * 通过 SSH 连接主机,基于 MySftpAccounts 凭证执行 docker 命令。
+ * 支持密码认证和私钥认证。
+ */
+public class DockerUtil {
+
+ private static final int SSH_TIMEOUT = 30000;
+
+ public static DockerResult start(MySftpAccounts account, String image, String name,
+ String[] ports, String[] envs) {
+ StringBuilder cmd = new StringBuilder("docker run -d");
+ if (name != null && !name.isEmpty()) {
+ cmd.append(" --name ").append(name);
+ }
+ if (ports != null) {
+ for (String p : ports) cmd.append(" -p ").append(p);
+ }
+ if (envs != null) {
+ for (String e : envs) cmd.append(" -e \"").append(e).append("\"");
+ }
+ cmd.append(" ").append(image);
+ return exec(account, cmd.toString());
+ }
+
+ public static DockerResult stop(MySftpAccounts account, String containerId) {
+ return exec(account, "docker stop " + containerId);
+ }
+
+ public static DockerResult restart(MySftpAccounts account, String containerId) {
+ return exec(account, "docker restart " + containerId);
+ }
+
+ public static DockerResult getLogs(MySftpAccounts account, String containerId,
+ int tail, boolean timestamps) {
+ String cmd = "docker logs" + (timestamps ? " -t" : "")
+ + " --tail " + tail + " " + containerId;
+ return exec(account, cmd);
+ }
+
+ public static DockerResult list(MySftpAccounts account, boolean all) {
+ return exec(account, all ? "docker ps -a" : "docker ps");
+ }
+
+ public static DockerResult remove(MySftpAccounts account, String containerId, boolean force) {
+ String cmd = force
+ ? "docker stop " + containerId + " && docker rm " + containerId
+ : "docker rm " + containerId;
+ return exec(account, cmd);
+ }
+
+ public static DockerResult inspect(MySftpAccounts account, String containerId) {
+ return exec(account, "docker inspect " + containerId);
+ }
+
+ public static DockerResult pull(MySftpAccounts account, String image) {
+ return exec(account, "docker pull " + image);
+ }
+
+ public static DockerResult version(MySftpAccounts account) {
+ return exec(account, "docker version --format '{{json .}}'");
+ }
+
+ public static List listContainers(MySftpAccounts account, boolean all) {
+ List list = new ArrayList<>();
+ String cmd = (all ? "docker ps -a" : "docker ps")
+ + " --format \"{{.ID}}|{{.Image}}|{{.Command}}|{{.CreatedAt}}|{{.Status}}|{{.Ports}}|{{.Names}}\"";
+ DockerResult r = exec(account, cmd);
+ if (!r.isSuccess()) {
+ return new ArrayList<>();
+ }
+ for (String line : r.getOutput().split("\n")) {
+ if (line.trim().isEmpty()) continue;
+ String[] p = line.split("\\|");
+ if (p.length < 2) continue;
+ ContainerInfo info = new ContainerInfo();
+ info.setContainerId(p.length > 0 ? p[0].trim() : "");
+ info.setImage(p.length > 1 ? p[1].trim() : "");
+ info.setCommand(p.length > 2 ? p[2].trim() : "");
+ info.setCreated(p.length > 3 ? p[3].trim() : "");
+ info.setStatus(p.length > 4 ? p[4].trim() : "");
+ info.setPorts(p.length > 5 ? p[5].trim() : "");
+ info.setNames(p.length > 6 ? p[6].trim() : "");
+ list.add(info);
+ }
+ return list;
+ }
+
+ public static DockerResult exec(MySftpAccounts account, String command) {
+ JSch jsch = new JSch();
+ Session session = null;
+ ChannelExec channel = null;
+ try {
+ Integer port = account.getHostPort() != null ? account.getHostPort() : 22;
+ session = jsch.getSession(account.getUsername(), account.getHostIp(), port);
+ session.setTimeout(SSH_TIMEOUT);
+
+ if ("key".equalsIgnoreCase(account.getAuthType())
+ && account.getPrivateKey() != null && !account.getPrivateKey().isEmpty()) {
+ jsch.addIdentity("temp",
+ account.getPrivateKey().getBytes(StandardCharsets.UTF_8),
+ null, null);
+ } else if (account.getPassword() != null && !account.getPassword().isEmpty()) {
+ session.setPassword(account.getPassword());
+ } else {
+ return DockerResult.fail("SSH 认证信息不完整:缺少密码或私钥");
+ }
+
+ java.util.Hashtable config = new java.util.Hashtable<>();
+ config.put("StrictHostKeyChecking", "no");
+ session.setConfig(config);
+ session.connect(SSH_TIMEOUT);
+
+ channel = (ChannelExec) session.openChannel("exec");
+ String finalCmd = (account.getRootPath() != null && !account.getRootPath().isEmpty())
+ ? "cd " + account.getRootPath() + " && " + command
+ : command;
+ channel.setCommand(finalCmd);
+ channel.setInputStream(null);
+
+ InputStream in = channel.getInputStream();
+ ByteArrayOutputStream errOut = new ByteArrayOutputStream();
+ channel.setExtOutputStream(errOut);
+
+ channel.connect();
+
+ StringBuilder outBuilder = new StringBuilder();
+ if (in != null) {
+ byte[] buf = new byte[4096];
+ int n;
+ while ((n = in.read(buf)) != -1) {
+ outBuilder.append(new String(buf, 0, n, StandardCharsets.UTF_8));
+ }
+ }
+
+ while (!channel.isClosed()) {
+ Thread.sleep(50);
+ }
+
+ String stdout = outBuilder.toString().trim();
+ String stderr = errOut.toString(StandardCharsets.UTF_8).trim();
+ int exitCode = channel.getExitStatus();
+
+ if (exitCode == 0) {
+ return DockerResult.ok(stdout);
+ } else {
+ return DockerResult.fail("命令执行失败,退出码: " + exitCode,
+ stderr.isEmpty() ? stdout : stderr);
+ }
+
+ } catch (JSchException e) {
+ return DockerResult.fail("SSH 连接失败: " + e.getMessage());
+ } catch (IOException e) {
+ return DockerResult.fail("IO 异常: " + e.getMessage());
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return DockerResult.fail("执行被中断");
+ } finally {
+ if (channel != null && channel.isConnected()) channel.disconnect();
+ if (session != null && session.isConnected()) session.disconnect();
+ }
+ }
+}
diff --git a/web-api/src/main/resources/mappings/modules/biz/MySftpAccountsDao.xml b/web-api/src/main/resources/mappings/modules/biz/MySftpAccountsDao.xml
new file mode 100644
index 0000000..7fb75b1
--- /dev/null
+++ b/web-api/src/main/resources/mappings/modules/biz/MySftpAccountsDao.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web-api/src/main/resources/mappings/modules/biz/MySftpHostsDao.xml b/web-api/src/main/resources/mappings/modules/biz/MySftpHostsDao.xml
new file mode 100644
index 0000000..63898a0
--- /dev/null
+++ b/web-api/src/main/resources/mappings/modules/biz/MySftpHostsDao.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web-vue/packages/biz/api/biz/mySftpAccounts.ts b/web-vue/packages/biz/api/biz/mySftpAccounts.ts
new file mode 100644
index 0000000..f1e57f4
--- /dev/null
+++ b/web-vue/packages/biz/api/biz/mySftpAccounts.ts
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2013-Now https://jeesite.com All rights reserved.
+ * No deletion without permission, or be held responsible to law.
+ * @author gaoxq
+ */
+import { BasicModel } from '@jeesite/core/api/model/baseModel';
+import { MySftpHosts } from '@jeesite/biz/api/biz/mySftpHosts';
+
+export interface MySftpAccounts extends BasicModel {
+ createTime?: string; // 记录时间
+ accountId?: string; // 账号标识
+ hostId: MySftpHosts; // 主机名称 父类
+ username: string; // 登录账号
+ password: string; // 登录密码
+ rootPath: string; // 初始目录
+ authType: string; // 认证方式
+ privateKey?: string; // 密钥内容
+ expireTime?: string; // 过期时间
+ accountRemark?: string; // 账号备注
+ updateTime?: string; // 更新时间
+ ustatus: string; // 状态
+}
diff --git a/web-vue/packages/biz/api/biz/mySftpHosts.ts b/web-vue/packages/biz/api/biz/mySftpHosts.ts
new file mode 100644
index 0000000..cdbbad1
--- /dev/null
+++ b/web-vue/packages/biz/api/biz/mySftpHosts.ts
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2013-Now https://jeesite.com All rights reserved.
+ * No deletion without permission, or be held responsible to law.
+ * @author gaoxq
+ */
+import { defHttp } from '@jeesite/core/utils/http/axios';
+import { useGlobSetting } from '@jeesite/core/hooks/setting';
+import { BasicModel, Page } from '@jeesite/core/api/model/baseModel';
+import { MySftpAccounts } from '@jeesite/biz/api/biz/mySftpAccounts';
+import { UploadApiResult } from '@jeesite/core/api/sys/upload';
+import { UploadFileParams } from '@jeesite/types/axios';
+import { AxiosProgressEvent } from 'axios';
+
+const { ctxPath, adminPath } = useGlobSetting();
+
+export interface MySftpHosts extends BasicModel {
+ createTime?: string; // 记录时间
+ hostId?: string; // 主机主键
+ hostIp: string; // 主机域名
+ hostPort: number; // 主机端口
+ hostName: string; // 主机名称
+ hostRemark?: string; // 备注说明
+ updateTime?: string; // 更新时间
+ ustatus: string; // 状态
+ mySftpAccountsList?: MySftpAccounts[]; // 子表列表
+}
+
+export const mySftpHostsList = (params?: MySftpHosts | any) =>
+ defHttp.get({ url: adminPath + '/biz/mySftpHosts/list', params });
+
+export const mySftpHostsListData = (params?: MySftpHosts | any) =>
+ defHttp.post>({ url: adminPath + '/biz/mySftpHosts/listData', params });
+
+export const mySftpHostsForm = (params?: MySftpHosts | any) =>
+ defHttp.get({ url: adminPath + '/biz/mySftpHosts/form', params });
+
+export const mySftpHostsSave = (params?: any, data?: MySftpHosts | any) =>
+ defHttp.postJson({ url: adminPath + '/biz/mySftpHosts/save', params, data });
+
+export const mySftpHostsImportData = (
+ params: UploadFileParams,
+ onUploadProgress: (progressEvent: AxiosProgressEvent) => void,
+) =>
+ defHttp.uploadFile(
+ {
+ url: ctxPath + adminPath + '/biz/mySftpHosts/importData',
+ onUploadProgress,
+ },
+ params,
+ );
+
+export const mySftpHostsDelete = (params?: MySftpHosts | any) =>
+ defHttp.get({ url: adminPath + '/biz/mySftpHosts/delete', params });
diff --git a/web-vue/packages/biz/views/biz/mySftpHosts/form.vue b/web-vue/packages/biz/views/biz/mySftpHosts/form.vue
new file mode 100644
index 0000000..e449493
--- /dev/null
+++ b/web-vue/packages/biz/views/biz/mySftpHosts/form.vue
@@ -0,0 +1,156 @@
+
+
+
+
+
+ {{ getTitle.value }}
+
+
+
+
+
+
+
+
+
diff --git a/web-vue/packages/biz/views/biz/mySftpHosts/formMySftpAccountsList.vue b/web-vue/packages/biz/views/biz/mySftpHosts/formMySftpAccountsList.vue
new file mode 100644
index 0000000..b4e07db
--- /dev/null
+++ b/web-vue/packages/biz/views/biz/mySftpHosts/formMySftpAccountsList.vue
@@ -0,0 +1,213 @@
+
+
+
+
+
diff --git a/web-vue/packages/biz/views/biz/mySftpHosts/list.vue b/web-vue/packages/biz/views/biz/mySftpHosts/list.vue
new file mode 100644
index 0000000..fc579ea
--- /dev/null
+++ b/web-vue/packages/biz/views/biz/mySftpHosts/list.vue
@@ -0,0 +1,207 @@
+
+
+
+
+
+
+ {{ getTitle.value }}
+
+
+
+ {{ t('导出') }}
+
+
+ {{ t('新增') }}
+
+
+
+
+ {{ text }}
+
+
+
+
+
+
+
diff --git a/web-vue/packages/biz/views/biz/mySftpHosts/select.ts b/web-vue/packages/biz/views/biz/mySftpHosts/select.ts
new file mode 100644
index 0000000..c0fe5ae
--- /dev/null
+++ b/web-vue/packages/biz/views/biz/mySftpHosts/select.ts
@@ -0,0 +1,110 @@
+import { useI18n } from '@jeesite/core/hooks/web/useI18n';
+import { BasicColumn, BasicTableProps, FormProps } from '@jeesite/core/components/Table';
+import { mySftpHostsListData } from '@jeesite/biz/api/biz/mySftpHosts';
+
+const { t } = useI18n('biz.mySftpHosts');
+
+const modalProps = {
+ title: t('主机选择'),
+};
+
+const searchForm: FormProps = {
+ baseColProps: { md: 8, lg: 6 },
+ labelWidth: 90,
+ schemas: [
+ {
+ label: t('主机域名'),
+ field: 'hostIp',
+ component: 'Input',
+ },
+ {
+ label: t('主机名称'),
+ field: 'hostName',
+ component: 'Input',
+ },
+ {
+ label: t('状态'),
+ field: 'ustatus',
+ component: 'Input',
+ },
+ ],
+};
+
+const tableColumns: BasicColumn[] = [
+ {
+ title: t('记录时间'),
+ dataIndex: 'createTime',
+ key: 'a.create_time',
+ sorter: true,
+ width: 230,
+ align: 'left',
+ slot: 'firstColumn',
+ },
+ {
+ title: t('主机域名'),
+ dataIndex: 'hostIp',
+ key: 'a.host_ip',
+ sorter: true,
+ width: 130,
+ align: 'left',
+ },
+ {
+ title: t('主机端口'),
+ dataIndex: 'hostPort',
+ key: 'a.host_port',
+ sorter: true,
+ width: 130,
+ align: 'center',
+ },
+ {
+ title: t('主机名称'),
+ dataIndex: 'hostName',
+ key: 'a.host_name',
+ sorter: true,
+ width: 130,
+ align: 'left',
+ },
+ {
+ title: t('备注说明'),
+ dataIndex: 'hostRemark',
+ key: 'a.host_remark',
+ sorter: true,
+ width: 130,
+ align: 'left',
+ },
+ {
+ title: t('更新时间'),
+ dataIndex: 'updateTime',
+ key: 'a.update_time',
+ sorter: true,
+ width: 130,
+ align: 'center',
+ },
+ {
+ title: t('状态'),
+ dataIndex: 'ustatus',
+ key: 'a.ustatus',
+ sorter: true,
+ width: 130,
+ align: 'left',
+ },
+];
+
+const tableProps: BasicTableProps = {
+ api: mySftpHostsListData,
+ beforeFetch: (params) => {
+ params['isAll'] = true;
+ return params;
+ },
+ columns: tableColumns,
+ formConfig: searchForm,
+ rowKey: 'hostId',
+};
+
+export default {
+ modalProps,
+ tableProps,
+ itemCode: 'hostId',
+ itemName: 'hostId',
+ isShowCode: false,
+};