diff --git a/modules/biz/pom.xml b/modules/biz/pom.xml index 09efe1e..5621af3 100644 --- a/modules/biz/pom.xml +++ b/modules/biz/pom.xml @@ -25,7 +25,7 @@ jeesite-module-core ${project.parent.version} - + diff --git a/web-api/src/main/java/com/jeesite/modules/apps/Module/ContainerInfo.java b/web-api/src/main/java/com/jeesite/modules/apps/Module/ContainerInfo.java new file mode 100644 index 0000000..1314b4f --- /dev/null +++ b/web-api/src/main/java/com/jeesite/modules/apps/Module/ContainerInfo.java @@ -0,0 +1,20 @@ +package com.jeesite.modules.apps.Module; + +import lombok.Data; + +import java.io.Serializable; + +/** + * Docker 容器信息 + */ +@Data +public class ContainerInfo implements Serializable { + + private String containerId; + private String image; + private String command; + private String status; + private String created; + private String names; + private String ports; +} diff --git a/web-api/src/main/java/com/jeesite/modules/apps/Module/DockerResult.java b/web-api/src/main/java/com/jeesite/modules/apps/Module/DockerResult.java new file mode 100644 index 0000000..b2ca02b --- /dev/null +++ b/web-api/src/main/java/com/jeesite/modules/apps/Module/DockerResult.java @@ -0,0 +1,36 @@ +package com.jeesite.modules.apps.Module; + +import lombok.Data; + +import java.io.Serializable; + +/** + * Docker 操作结果 + */ +@Data +public class DockerResult implements Serializable { + + private boolean success; + private String output; + private String error; + private String message; + + public static DockerResult ok(String output) { + DockerResult r = new DockerResult(); + r.success = true; + r.output = output; + return r; + } + + public static DockerResult fail(String message, String error) { + DockerResult r = new DockerResult(); + r.success = false; + r.message = message; + r.error = error; + return r; + } + + public static DockerResult fail(String message) { + return fail(message, null); + } +} diff --git a/web-api/src/main/java/com/jeesite/modules/biz/dao/MySftpAccountsDao.java b/web-api/src/main/java/com/jeesite/modules/biz/dao/MySftpAccountsDao.java new file mode 100644 index 0000000..077968e --- /dev/null +++ b/web-api/src/main/java/com/jeesite/modules/biz/dao/MySftpAccountsDao.java @@ -0,0 +1,15 @@ +package com.jeesite.modules.biz.dao; + +import com.jeesite.common.dao.CrudDao; +import com.jeesite.common.mybatis.annotation.MyBatisDao; +import com.jeesite.modules.biz.entity.MySftpAccounts; + +/** + * 主机信息表 DAO 接口 + * @author gaoxq + * @version 2026-04-12 + */ +@MyBatisDao(dataSourceName="work") +public interface MySftpAccountsDao extends CrudDao { + +} \ No newline at end of file diff --git a/web-api/src/main/java/com/jeesite/modules/biz/dao/MySftpHostsDao.java b/web-api/src/main/java/com/jeesite/modules/biz/dao/MySftpHostsDao.java new file mode 100644 index 0000000..0dde309 --- /dev/null +++ b/web-api/src/main/java/com/jeesite/modules/biz/dao/MySftpHostsDao.java @@ -0,0 +1,15 @@ +package com.jeesite.modules.biz.dao; + +import com.jeesite.common.dao.CrudDao; +import com.jeesite.common.mybatis.annotation.MyBatisDao; +import com.jeesite.modules.biz.entity.MySftpHosts; + +/** + * 主机信息表 DAO 接口 + * @author gaoxq + * @version 2026-04-12 + */ +@MyBatisDao(dataSourceName="work") +public interface MySftpHostsDao extends CrudDao { + +} \ No newline at end of file diff --git a/web-api/src/main/java/com/jeesite/modules/biz/entity/MySftpAccounts.java b/web-api/src/main/java/com/jeesite/modules/biz/entity/MySftpAccounts.java new file mode 100644 index 0000000..9b850ad --- /dev/null +++ b/web-api/src/main/java/com/jeesite/modules/biz/entity/MySftpAccounts.java @@ -0,0 +1,200 @@ +package com.jeesite.modules.biz.entity; + +import java.io.Serializable; +import java.util.Date; + +import com.jeesite.common.mybatis.annotation.JoinTable; +import com.jeesite.common.mybatis.annotation.JoinTable.Type; +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +import com.jeesite.common.entity.DataEntity; +import com.jeesite.common.mybatis.annotation.Column; +import com.jeesite.common.mybatis.annotation.Table; +import com.jeesite.common.mybatis.mapper.query.QueryType; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serial; + +/** + * 主机信息表 Entity + * + * @author gaoxq + * @version 2026-04-12 + */ +@EqualsAndHashCode(callSuper = true) +@Table(name = "my_sftp_accounts", alias = "a", label = "主机信息", columns = { + @Column(name = "create_time", attrName = "createTime", label = "记录时间", isUpdate = false, isUpdateForce = true), + @Column(name = "account_id", attrName = "accountId", label = "账号标识", isPK = true), + @Column(name = "host_id", attrName = "hostId.hostId", label = "主机名称"), + @Column(name = "username", attrName = "username", label = "登录账号"), + @Column(name = "password", attrName = "password", label = "登录密码", isQuery = false), + @Column(name = "root_path", attrName = "rootPath", label = "初始目录", isQuery = false), + @Column(name = "auth_type", attrName = "authType", label = "认证方式"), + @Column(name = "private_key", attrName = "privateKey", label = "密钥内容", isQuery = false), + @Column(name = "status", attrName = "status", label = "账号状态", isUpdate = false, isQuery = false), + @Column(name = "expire_time", attrName = "expireTime", label = "过期时间", isQuery = false, isUpdateForce = true), + @Column(name = "account_remark", attrName = "accountRemark", label = "账号备注"), + @Column(name = "update_time", attrName = "updateTime", label = "更新时间", isQuery = false, isUpdateForce = true), + @Column(name = "ustatus", attrName = "ustatus", label = "状态"), +}, orderBy = "a.create_time DESC" +) +@Data +public class MySftpAccounts extends DataEntity implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + private Date createTime; // 记录时间 + private String accountId; // 账号标识 + private MySftpHosts hostId; // 主机名称 父类 + private String username; // 登录账号 + private String password; // 登录密码 + private String rootPath; // 初始目录 + private String authType; // 认证方式 + private String privateKey; // 密钥内容 + private Date expireTime; // 过期时间 + private String accountRemark; // 账号备注 + private Date updateTime; // 更新时间 + private String ustatus; // 状态 + + private String hostIp; // 主机域名 + private Integer hostPort; // 主机端口 + private String hostName; // 主机名称 + + public MySftpAccounts() { + this(null); + } + + public MySftpAccounts(MySftpHosts hostId) { + this.hostId = hostId; + } + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } + + public String getAccountId() { + return accountId; + } + + public void setAccountId(String accountId) { + this.accountId = accountId; + } + + public MySftpHosts getHostId() { + return hostId; + } + + public void setHostId(MySftpHosts hostId) { + this.hostId = hostId; + } + + @NotBlank(message = "登录账号不能为空") + @Size(min = 0, max = 100, message = "登录账号长度不能超过 100 个字符") + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + @NotBlank(message = "登录密码不能为空") + @Size(min = 0, max = 255, message = "登录密码长度不能超过 255 个字符") + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + @NotBlank(message = "初始目录不能为空") + @Size(min = 0, max = 255, message = "初始目录长度不能超过 255 个字符") + public String getRootPath() { + return rootPath; + } + + public void setRootPath(String rootPath) { + this.rootPath = rootPath; + } + + @NotBlank(message = "认证方式不能为空") + @Size(min = 0, max = 12, message = "认证方式长度不能超过 12 个字符") + public String getAuthType() { + return authType; + } + + public void setAuthType(String authType) { + this.authType = authType; + } + + public String getPrivateKey() { + return privateKey; + } + + public void setPrivateKey(String privateKey) { + this.privateKey = privateKey; + } + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + public Date getExpireTime() { + return expireTime; + } + + public void setExpireTime(Date expireTime) { + this.expireTime = expireTime; + } + + @Size(min = 0, max = 255, message = "账号备注长度不能超过 255 个字符") + public String getAccountRemark() { + return accountRemark; + } + + public void setAccountRemark(String accountRemark) { + this.accountRemark = accountRemark; + } + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + public Date getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Date updateTime) { + this.updateTime = updateTime; + } + + @NotBlank(message = "状态不能为空") + @Size(min = 0, max = 12, message = "状态长度不能超过 12 个字符") + public String getUstatus() { + return ustatus; + } + + public void setUstatus(String ustatus) { + this.ustatus = ustatus; + } + + public Date getCreateTime_gte() { + return sqlMap.getWhere().getValue("create_time", QueryType.GTE); + } + + public void setCreateTime_gte(Date createTime) { + sqlMap.getWhere().and("create_time", QueryType.GTE, createTime); + } + + public Date getCreateTime_lte() { + return sqlMap.getWhere().getValue("create_time", QueryType.LTE); + } + + public void setCreateTime_lte(Date createTime) { + sqlMap.getWhere().and("create_time", QueryType.LTE, createTime); + } + +} \ No newline at end of file diff --git a/web-api/src/main/java/com/jeesite/modules/biz/entity/MySftpHosts.java b/web-api/src/main/java/com/jeesite/modules/biz/entity/MySftpHosts.java new file mode 100644 index 0000000..d22c710 --- /dev/null +++ b/web-api/src/main/java/com/jeesite/modules/biz/entity/MySftpHosts.java @@ -0,0 +1,161 @@ +package com.jeesite.modules.biz.entity; + +import jakarta.validation.Valid; + +import java.io.Serializable; +import java.util.Date; +import com.jeesite.common.mybatis.annotation.JoinTable; +import com.jeesite.common.mybatis.annotation.JoinTable.Type; +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import com.jeesite.common.collect.ListUtils; + +import com.jeesite.common.entity.DataEntity; +import com.jeesite.common.mybatis.annotation.Column; +import com.jeesite.common.mybatis.annotation.Table; +import com.jeesite.common.mybatis.mapper.query.QueryType; +import com.jeesite.common.utils.excel.annotation.ExcelField; +import com.jeesite.common.utils.excel.annotation.ExcelField.Align; +import com.jeesite.common.utils.excel.annotation.ExcelFields; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serial; + +/** + * 主机信息表 Entity + * @author gaoxq + * @version 2026-04-12 + */ +@EqualsAndHashCode(callSuper = true) +@Table(name="my_sftp_hosts", alias="a", label="主机信息", columns={ + @Column(name="create_time", attrName="createTime", label="记录时间", isUpdate=false, isQuery=false, isUpdateForce=true), + @Column(name="host_id", attrName="hostId", label="主机主键", isPK=true), + @Column(name="host_ip", attrName="hostIp", label="主机域名", queryType=QueryType.LIKE), + @Column(name="host_port", attrName="hostPort", label="主机端口", isQuery=false), + @Column(name="host_name", attrName="hostName", label="主机名称", queryType=QueryType.LIKE), + @Column(name="host_remark", attrName="hostRemark", label="备注说明", isQuery=false), + @Column(name="update_time", attrName="updateTime", label="更新时间", isQuery=false, isUpdateForce=true), + @Column(name="ustatus", attrName="ustatus", label="状态"), + }, orderBy="a.create_time DESC" +) +@Data +public class MySftpHosts extends DataEntity implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + private Date createTime; // 记录时间 + private String hostId; // 主机主键 + private String hostIp; // 主机域名 + private Integer hostPort; // 主机端口 + private String hostName; // 主机名称 + private String hostRemark; // 备注说明 + private Date updateTime; // 更新时间 + private String ustatus; // 状态 + private List mySftpAccountsList = ListUtils.newArrayList(); // 子表列表 + + @ExcelFields({ + @ExcelField(title="记录时间", attrName="createTime", align=Align.CENTER, sort=10, dataFormat="yyyy-MM-dd hh:mm"), + @ExcelField(title="主机主键", attrName="hostId", align=Align.CENTER, sort=20), + @ExcelField(title="主机域名", attrName="hostIp", align=Align.CENTER, sort=30), + @ExcelField(title="主机端口", attrName="hostPort", align=Align.CENTER, sort=40), + @ExcelField(title="主机名称", attrName="hostName", align=Align.CENTER, sort=50), + @ExcelField(title="备注说明", attrName="hostRemark", align=Align.CENTER, sort=60), + @ExcelField(title="更新时间", attrName="updateTime", align=Align.CENTER, sort=70, dataFormat="yyyy-MM-dd hh:mm"), + @ExcelField(title="状态", attrName="ustatus", align=Align.CENTER, sort=80), + }) + public MySftpHosts() { + this(null); + } + + public MySftpHosts(String id){ + super(id); + } + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } + + public String getHostId() { + return hostId; + } + + public void setHostId(String hostId) { + this.hostId = hostId; + } + + @NotBlank(message="主机域名不能为空") + @Size(min=0, max=64, message="主机域名长度不能超过 64 个字符") + public String getHostIp() { + return hostIp; + } + + public void setHostIp(String hostIp) { + this.hostIp = hostIp; + } + + @NotNull(message="主机端口不能为空") + public Integer getHostPort() { + return hostPort; + } + + public void setHostPort(Integer hostPort) { + this.hostPort = hostPort; + } + + @NotBlank(message="主机名称不能为空") + @Size(min=0, max=100, message="主机名称长度不能超过 100 个字符") + public String getHostName() { + return hostName; + } + + public void setHostName(String hostName) { + this.hostName = hostName; + } + + @Size(min=0, max=255, message="备注说明长度不能超过 255 个字符") + public String getHostRemark() { + return hostRemark; + } + + public void setHostRemark(String hostRemark) { + this.hostRemark = hostRemark; + } + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + public Date getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Date updateTime) { + this.updateTime = updateTime; + } + + @NotBlank(message="状态不能为空") + @Size(min=0, max=12, message="状态长度不能超过 12 个字符") + public String getUstatus() { + return ustatus; + } + + public void setUstatus(String ustatus) { + this.ustatus = ustatus; + } + + @Valid + public List getMySftpAccountsList() { + return mySftpAccountsList; + } + + public void setMySftpAccountsList(List mySftpAccountsList) { + this.mySftpAccountsList = mySftpAccountsList; + } + +} \ No newline at end of file diff --git a/web-api/src/main/java/com/jeesite/modules/biz/service/MySftpHostsService.java b/web-api/src/main/java/com/jeesite/modules/biz/service/MySftpHostsService.java new file mode 100644 index 0000000..44257c4 --- /dev/null +++ b/web-api/src/main/java/com/jeesite/modules/biz/service/MySftpHostsService.java @@ -0,0 +1,175 @@ +package com.jeesite.modules.biz.service; + +import java.util.List; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.jeesite.common.entity.Page; +import com.jeesite.common.service.CrudService; +import com.jeesite.modules.biz.entity.MySftpHosts; +import com.jeesite.modules.biz.dao.MySftpHostsDao; +import com.jeesite.common.service.ServiceException; +import com.jeesite.modules.biz.entity.MySftpAccounts; +import com.jeesite.modules.biz.dao.MySftpAccountsDao; +import com.jeesite.common.config.Global; +import com.jeesite.common.validator.ValidatorUtils; +import com.jeesite.common.utils.excel.ExcelImport; +import org.springframework.web.multipart.MultipartFile; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; + +/** + * 主机信息表 Service + * @author gaoxq + * @version 2026-04-12 + */ +@Service +public class MySftpHostsService extends CrudService { + + private final MySftpAccountsDao mySftpAccountsDao; + + public MySftpHostsService(MySftpAccountsDao mySftpAccountsDao) { + this.mySftpAccountsDao = mySftpAccountsDao; + } + + /** + * 获取单条数据 + * @param mySftpHosts 主键 + */ + @Override + public MySftpHosts get(MySftpHosts mySftpHosts) { + MySftpHosts entity = super.get(mySftpHosts); + if (entity != null){ + MySftpAccounts mySftpAccounts = new MySftpAccounts(entity); + mySftpAccounts.setStatus(MySftpAccounts.STATUS_NORMAL); + entity.setMySftpAccountsList(mySftpAccountsDao.findList(mySftpAccounts)); + } + return entity; + } + + /** + * 查询分页数据 + * @param mySftpHosts 查询条件 + * @param mySftpHosts page 分页对象 + */ + @Override + public Page findPage(MySftpHosts mySftpHosts) { + return super.findPage(mySftpHosts); + } + + /** + * 查询列表数据 + * @param mySftpHosts 查询条件 + */ + @Override + public List findList(MySftpHosts mySftpHosts) { + return super.findList(mySftpHosts); + } + + /** + * 查询子表分页数据 + * @param mySftpAccounts + * @param mySftpAccounts page 分页对象 + */ + public Page findSubPage(MySftpAccounts mySftpAccounts) { + Page page = mySftpAccounts.getPage(); + page.setList(mySftpAccountsDao.findList(mySftpAccounts)); + return page; + } + + /** + * 保存数据(插入或更新) + * @param mySftpHosts 数据对象 + */ + @Override + @Transactional + public void save(MySftpHosts mySftpHosts) { + super.save(mySftpHosts); + // 保存 MySftpHosts子表 + for (MySftpAccounts mySftpAccounts : mySftpHosts.getMySftpAccountsList()){ + if (!MySftpAccounts.STATUS_DELETE.equals(mySftpAccounts.getStatus())){ + mySftpAccounts.setHostId(mySftpHosts); + if (mySftpAccounts.getIsNewRecord()){ + mySftpAccountsDao.insert(mySftpAccounts); + }else{ + mySftpAccountsDao.update(mySftpAccounts); + } + }else{ + mySftpAccountsDao.delete(mySftpAccounts); + } + } + } + + /** + * 导入数据 + * @param file 导入的数据文件 + */ + @Transactional + public String importData(MultipartFile file) { + if (file == null){ + throw new ServiceException(text("请选择导入的数据文件!")); + } + int successNum = 0; int failureNum = 0; + StringBuilder successMsg = new StringBuilder(); + StringBuilder failureMsg = new StringBuilder(); + try(ExcelImport ei = new ExcelImport(file, 2, 0)){ + List list = ei.getDataList(MySftpHosts.class); + for (MySftpHosts mySftpHosts : list) { + try{ + ValidatorUtils.validateWithException(mySftpHosts); + this.save(mySftpHosts); + successNum++; + successMsg.append("
" + successNum + "、编号 " + mySftpHosts.getId() + " 导入成功"); + } catch (Exception e) { + failureNum++; + String msg = "
" + failureNum + "、编号 " + mySftpHosts.getId() + " 导入失败:"; + if (e instanceof ConstraintViolationException){ + ConstraintViolationException cve = (ConstraintViolationException)e; + for (ConstraintViolation violation : cve.getConstraintViolations()) { + msg += Global.getText(violation.getMessage()) + " ("+violation.getPropertyPath()+")"; + } + }else{ + msg += e.getMessage(); + } + failureMsg.append(msg); + logger.error(msg, e); + } + } + } catch (Exception e) { + logger.error(e.getMessage(), e); + failureMsg.append(e.getMessage()); + return failureMsg.toString(); + } + if (failureNum > 0) { + failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:"); + throw new ServiceException(failureMsg.toString()); + }else{ + successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:"); + } + return successMsg.toString(); + } + + /** + * 更新状态 + * @param mySftpHosts 数据对象 + */ + @Override + @Transactional + public void updateStatus(MySftpHosts mySftpHosts) { + super.updateStatus(mySftpHosts); + } + + /** + * 删除数据 + * @param mySftpHosts 数据对象 + */ + @Override + @Transactional + public void delete(MySftpHosts mySftpHosts) { + super.delete(mySftpHosts); + MySftpAccounts mySftpAccounts = new MySftpAccounts(); + mySftpAccounts.setHostId(mySftpHosts); + mySftpAccountsDao.deleteByEntity(mySftpAccounts); + } + +} \ No newline at end of file diff --git a/web-api/src/main/java/com/jeesite/modules/biz/web/MySftpHostsController.java b/web-api/src/main/java/com/jeesite/modules/biz/web/MySftpHostsController.java new file mode 100644 index 0000000..a429e74 --- /dev/null +++ b/web-api/src/main/java/com/jeesite/modules/biz/web/MySftpHostsController.java @@ -0,0 +1,159 @@ +package com.jeesite.modules.biz.web; + +import java.util.List; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import com.jeesite.common.config.Global; +import com.jeesite.common.collect.ListUtils; +import com.jeesite.common.entity.Page; +import com.jeesite.common.lang.DateUtils; +import com.jeesite.common.utils.excel.ExcelExport; +import com.jeesite.common.utils.excel.annotation.ExcelField.Type; +import org.springframework.web.multipart.MultipartFile; +import com.jeesite.common.web.BaseController; +import com.jeesite.modules.biz.entity.MySftpHosts; +import com.jeesite.modules.biz.entity.MySftpAccounts; +import com.jeesite.modules.biz.service.MySftpHostsService; + +/** + * 主机信息表 Controller + * @author gaoxq + * @version 2026-04-12 + */ +@Controller +@RequestMapping(value = "${adminPath}/biz/mySftpHosts") +public class MySftpHostsController extends BaseController { + + private final MySftpHostsService mySftpHostsService; + + public MySftpHostsController(MySftpHostsService mySftpHostsService) { + this.mySftpHostsService = mySftpHostsService; + } + + /** + * 获取数据 + */ + @ModelAttribute + public MySftpHosts get(String hostId, boolean isNewRecord) { + return mySftpHostsService.get(hostId, isNewRecord); + } + + /** + * 查询列表 + */ + @RequiresPermissions("biz:mySftpHosts:view") + @RequestMapping(value = {"list", ""}) + public String list(MySftpHosts mySftpHosts, Model model) { + model.addAttribute("mySftpHosts", mySftpHosts); + return "modules/biz/mySftpHostsList"; + } + + /** + * 查询列表数据 + */ + @RequiresPermissions("biz:mySftpHosts:view") + @RequestMapping(value = "listData") + @ResponseBody + public Page listData(MySftpHosts mySftpHosts, HttpServletRequest request, HttpServletResponse response) { + mySftpHosts.setPage(new Page<>(request, response)); + Page page = mySftpHostsService.findPage(mySftpHosts); + return page; + } + + /** + * 查询子表数据 + */ + @RequiresPermissions("biz:mySftpHosts:view") + @RequestMapping(value = "mySftpAccountsListData") + @ResponseBody + public Page subListData(MySftpAccounts mySftpAccounts, HttpServletRequest request, HttpServletResponse response) { + mySftpAccounts.setPage(new Page<>(request, response)); + Page page = mySftpHostsService.findSubPage(mySftpAccounts); + return page; + } + + /** + * 查看编辑表单 + */ + @RequiresPermissions("biz:mySftpHosts:view") + @RequestMapping(value = "form") + public String form(MySftpHosts mySftpHosts, Model model) { + model.addAttribute("mySftpHosts", mySftpHosts); + return "modules/biz/mySftpHostsForm"; + } + + /** + * 保存数据 + */ + @RequiresPermissions("biz:mySftpHosts:edit") + @PostMapping(value = "save") + @ResponseBody + public String save(@Validated MySftpHosts mySftpHosts) { + mySftpHostsService.save(mySftpHosts); + return renderResult(Global.TRUE, text("保存主机成功!")); + } + + /** + * 导出数据 + */ + @RequiresPermissions("biz:mySftpHosts:view") + @RequestMapping(value = "exportData") + public void exportData(MySftpHosts mySftpHosts, HttpServletResponse response) { + List list = mySftpHostsService.findList(mySftpHosts); + String fileName = "主机" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx"; + try(ExcelExport ee = new ExcelExport("主机", MySftpHosts.class)){ + ee.setDataList(list).write(response, fileName); + } + } + + /** + * 下载模板 + */ + @RequiresPermissions("biz:mySftpHosts:view") + @RequestMapping(value = "importTemplate") + public void importTemplate(HttpServletResponse response) { + MySftpHosts mySftpHosts = new MySftpHosts(); + List 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 @@ + + + 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 @@ + + + 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, +};